Init
This commit is contained in:
3
main/CMakeLists.txt
Normal file
3
main/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "main.cpp"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES mbedtls esp-cryptoauthlib esp_http_server esp_wifi nvs_flash json esp_timer efuse)
|
||||
15
main/Kconfig.projbuild
Normal file
15
main/Kconfig.projbuild
Normal file
@@ -0,0 +1,15 @@
|
||||
menu "Wi-Fi Credentials"
|
||||
|
||||
config WIFI_SSID
|
||||
string "Wi-Fi SSID"
|
||||
default ""
|
||||
help
|
||||
SSID of the Wi-Fi network to connect to.
|
||||
|
||||
config WIFI_PASSWORD
|
||||
string "Wi-Fi Password"
|
||||
default ""
|
||||
help
|
||||
Password for the Wi-Fi network.
|
||||
|
||||
endmenu
|
||||
241
main/TangServer.h
Normal file
241
main/TangServer.h
Normal file
@@ -0,0 +1,241 @@
|
||||
#ifndef TANG_SERVER_H
|
||||
#define TANG_SERVER_H
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include <esp_event.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <string>
|
||||
|
||||
static const char *TAG = "TangServer";
|
||||
|
||||
// Include core components
|
||||
#include "atecc608a.h"
|
||||
#include "crypto.h"
|
||||
#include "encoding.h"
|
||||
#include "provision.h"
|
||||
#include "provision_handlers.h"
|
||||
#include "tang_handlers.h"
|
||||
#include "tang_storage.h"
|
||||
#include "zk_auth.h"
|
||||
#include "zk_handlers.h"
|
||||
|
||||
// --- Configuration ---
|
||||
const char *wifi_ssid = CONFIG_WIFI_SSID;
|
||||
const char *wifi_password = CONFIG_WIFI_PASSWORD;
|
||||
|
||||
// --- Global State ---
|
||||
bool unlocked = false; // Start inactive until provisioned and authenticated
|
||||
httpd_handle_t server_http = NULL;
|
||||
TangKeyStore keystore;
|
||||
ZKAuth zk_auth; // Zero-Knowledge Authentication
|
||||
|
||||
// WiFi event group
|
||||
static EventGroupHandle_t wifi_event_group;
|
||||
const int WIFI_CONNECTED_BIT = BIT0;
|
||||
|
||||
// --- WiFi Event Handler ---
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data) {
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
ESP_LOGI(TAG, "WiFi connecting...");
|
||||
} else if (event_base == WIFI_EVENT &&
|
||||
event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
esp_wifi_connect();
|
||||
ESP_LOGI(TAG, "WiFi disconnected, reconnecting...");
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "WiFi connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
// --- WiFi Setup ---
|
||||
void setup_wifi() {
|
||||
// Initialize event group
|
||||
wifi_event_group = xEventGroupCreate();
|
||||
|
||||
// Initialize network interface
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
|
||||
assert(sta_netif);
|
||||
|
||||
// Set hostname
|
||||
ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, "esp-tang-lol"));
|
||||
|
||||
// Initialize WiFi
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
// Register event handlers
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler, NULL));
|
||||
|
||||
// Configure WiFi
|
||||
wifi_config_t wifi_config = {};
|
||||
if (strlen(wifi_ssid) > 0) {
|
||||
strncpy((char *)wifi_config.sta.ssid, wifi_ssid,
|
||||
sizeof(wifi_config.sta.ssid));
|
||||
strncpy((char *)wifi_config.sta.password, wifi_password,
|
||||
sizeof(wifi_config.sta.password));
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_ssid);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "No WiFi SSID configured");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Initial Setup ---
|
||||
bool perform_initial_setup() {
|
||||
|
||||
if (!P256::generate_keypair(keystore.exc_pub, keystore.exc_priv)) {
|
||||
ESP_LOGE(TAG, "ERROR: Failed to generate exchange key");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save Tang keys directly (no encryption in prototype)
|
||||
if (!keystore.save_tang_keys()) {
|
||||
ESP_LOGE(TAG, "ERROR: Failed to save Tang keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Configuration saved to NVS");
|
||||
|
||||
ESP_LOGI(TAG, "=======================================================");
|
||||
ESP_LOGI(TAG, "Setup complete! Device is ready to use");
|
||||
ESP_LOGI(TAG, "NOTE: Exchange key stored unencrypted for prototyping");
|
||||
ESP_LOGI(TAG, "=======================================================");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Setup HTTP Server Routes ---
|
||||
httpd_handle_t setup_http_server() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.lru_purge_enable = true;
|
||||
config.stack_size = 8192;
|
||||
config.max_uri_handlers = 16;
|
||||
|
||||
httpd_handle_t server = NULL;
|
||||
|
||||
if (httpd_start(&server, &config) == ESP_OK) {
|
||||
register_provision_handlers(server);
|
||||
|
||||
register_zk_handlers(server);
|
||||
|
||||
// Register Tang protocol handlers
|
||||
httpd_uri_t adv_uri = {.uri = "/adv",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_adv,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &adv_uri);
|
||||
|
||||
httpd_uri_t adv_uri_slash = {.uri = "/adv/",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_adv,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &adv_uri_slash);
|
||||
|
||||
httpd_uri_t rec_uri = {.uri = "/rec",
|
||||
.method = HTTP_POST,
|
||||
.handler = handle_rec,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &rec_uri);
|
||||
|
||||
httpd_uri_t config_uri = {.uri = "/config",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_config,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &config_uri);
|
||||
|
||||
httpd_uri_t reboot_uri = {.uri = "/reboot",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_reboot,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &reboot_uri);
|
||||
|
||||
// Register custom error handler for 404
|
||||
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, handle_not_found);
|
||||
|
||||
ESP_LOGI(TAG, "HTTP server listening on port 80");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to start HTTP server");
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
// --- Main Setup ---
|
||||
void setup() {
|
||||
ESP_LOGI(TAG, "\n\nESP32 Tang Server Starting...");
|
||||
|
||||
// Initialize NVS (required before any storage operations)
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
|
||||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
ESP_LOGI(TAG, "NVS initialized");
|
||||
|
||||
// Initialize ATECC608A
|
||||
if (atecc608B_init()) {
|
||||
atecc608B_print_config();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed");
|
||||
}
|
||||
|
||||
// Load or initialize configuration
|
||||
if (keystore.is_configured()) {
|
||||
ESP_LOGI(TAG, "Found existing configuration");
|
||||
// Auto-load Tang keys on startup (no activation needed in prototype)
|
||||
if (keystore.load_tang_keys()) {
|
||||
ESP_LOGI(TAG, "Loaded Tang keys - server ready");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to load Tang keys");
|
||||
}
|
||||
} else {
|
||||
perform_initial_setup();
|
||||
}
|
||||
|
||||
// Initialize Zero-Knowledge Authentication
|
||||
ESP_LOGI(TAG, "Initializing Zero-Knowledge Authentication...");
|
||||
if (zk_auth.init()) {
|
||||
ESP_LOGI(TAG, "ZK Auth initialized successfully");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "ZK Auth initialization failed");
|
||||
}
|
||||
|
||||
setup_wifi();
|
||||
server_http = setup_http_server();
|
||||
|
||||
if (server_http) {
|
||||
ESP_LOGI(TAG, "HTTP server listening on port 80");
|
||||
ESP_LOGI(TAG, " - ZK Auth UI: http://<ip>/");
|
||||
ESP_LOGI(TAG, " - Tang Server: http://<ip>/adv");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Loop ---
|
||||
void loop() {
|
||||
// Just delay - HTTP server handles requests in its own task
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
#endif // TANG_SERVER_H
|
||||
418
main/atecc608a.h
Normal file
418
main/atecc608a.h
Normal file
@@ -0,0 +1,418 @@
|
||||
#ifndef ATECC608B_H
|
||||
#define ATECC608B_H
|
||||
|
||||
#include "cryptoauthlib.h"
|
||||
#include "sdkconfig.h"
|
||||
#include <cJSON.h>
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
static const char *TAG_ATECC = "ATECC608B";
|
||||
|
||||
// ATECC608B Configuration
|
||||
ATCAIfaceCfg atecc_cfg;
|
||||
|
||||
bool atecc608B_init() {
|
||||
ESP_LOGI(TAG_ATECC, "\n=== ATECC608B Initialization ===");
|
||||
|
||||
// Configure cryptoauthlib for ATECC608B - it will handle I2C initialization
|
||||
atecc_cfg.iface_type = ATCA_I2C_IFACE;
|
||||
atecc_cfg.devtype = ATECC608B;
|
||||
atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS;
|
||||
atecc_cfg.atcai2c.bus = 0; // I2C bus number
|
||||
atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE;
|
||||
atecc_cfg.wake_delay = 1500;
|
||||
atecc_cfg.rx_retries = 20;
|
||||
|
||||
ESP_LOGI(TAG_ATECC,
|
||||
"Configuring ATECC608B: SDA=GPIO%d, SCL=GPIO%d, Address=0x%02X "
|
||||
"(7-bit: 0x%02X)",
|
||||
CONFIG_ATCA_I2C_SDA_PIN, CONFIG_ATCA_I2C_SCL_PIN,
|
||||
CONFIG_ATCA_I2C_ADDRESS, CONFIG_ATCA_I2C_ADDRESS >> 1);
|
||||
|
||||
// Initialize cryptoauthlib (it will initialize I2C internally)
|
||||
ATCA_STATUS status = atcab_init(&atecc_cfg);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_ATECC, "ERROR: atcab_init failed with status 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_ATECC, "ATECC608B initialized successfully");
|
||||
|
||||
// Give the device a moment to stabilize
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
// Wake the device and test communication
|
||||
status = atcab_wakeup();
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGW(TAG_ATECC, "WARNING: Wake command returned status 0x%02X", status);
|
||||
}
|
||||
|
||||
// Put device to idle
|
||||
atcab_idle();
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Global storage for config zone data
|
||||
uint8_t g_atecc_config_data[128] = {0};
|
||||
bool g_atecc_config_valid = false;
|
||||
|
||||
/**
|
||||
* Read ATECC608B configuration zone into global buffer
|
||||
*/
|
||||
bool atecc608B_read_config() {
|
||||
ESP_LOGI(TAG_ATECC, "Reading ATECC608B configuration zone...");
|
||||
|
||||
ATCA_STATUS status = atcab_read_config_zone(g_atecc_config_data);
|
||||
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read config zone, status 0x%02X",
|
||||
status);
|
||||
|
||||
// Try reading it in blocks as a workaround
|
||||
ESP_LOGI(TAG_ATECC, "Attempting to read config in 4-byte blocks...");
|
||||
bool read_success = true;
|
||||
for (uint8_t block = 0; block < 32; block++) {
|
||||
status = atcab_read_zone(ATCA_ZONE_CONFIG, 0, block, 0,
|
||||
&g_atecc_config_data[block * 4], 4);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read block %d, status 0x%02X",
|
||||
block, status);
|
||||
read_success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!read_success) {
|
||||
ESP_LOGE(TAG_ATECC, "ERROR: Could not read configuration zone");
|
||||
g_atecc_config_valid = false;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG_ATECC, "Successfully read config in blocks");
|
||||
}
|
||||
|
||||
g_atecc_config_valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print ATECC608B configuration zone to console with detailed subzone breakdown
|
||||
* According to ATECC608B Table 2-4
|
||||
*/
|
||||
void atecc608B_print_config() {
|
||||
ESP_LOGI(TAG_ATECC, "\n=== ATECC608B Configuration Zone ===");
|
||||
|
||||
// First, try to read the serial number as a simple communication test
|
||||
uint8_t serial_number[9];
|
||||
ATCA_STATUS status = atcab_read_serial_number(serial_number);
|
||||
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_ATECC, "ERROR: Failed to read serial number, status 0x%02X",
|
||||
status);
|
||||
ESP_LOGE(TAG_ATECC, "This might indicate a communication or wiring issue.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read configuration zone into global buffer
|
||||
if (!atecc608B_read_config()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *config_data = g_atecc_config_data;
|
||||
|
||||
// Print complete hex dump first
|
||||
ESP_LOGI(TAG_ATECC, "\n--- Complete Configuration Zone (128 bytes) ---");
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (i % 16 == 0) {
|
||||
printf("\n0x%02X: ", i);
|
||||
}
|
||||
printf("%02X ", config_data[i]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// Print detailed subzone breakdown according to Table 2-4
|
||||
ESP_LOGI(TAG_ATECC, "\n--- Subzone Breakdown (Table 2-4) ---");
|
||||
|
||||
// Bytes 0-3: Serial Number[0:3]
|
||||
printf("\n[Bytes 0-3] Serial Number[0:3]: ");
|
||||
for (int i = 0; i < 4; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Bytes 4-7: Revision Number
|
||||
printf("[Bytes 4-7] Revision Number: ");
|
||||
for (int i = 4; i < 8; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Bytes 8-12: Serial Number[4:8]
|
||||
printf("[Bytes 8-12] Serial Number[4:8]: ");
|
||||
for (int i = 8; i < 13; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Full Serial Number
|
||||
printf(" --> Complete Serial Number: ");
|
||||
for (int i = 0; i < 4; i++)
|
||||
printf("%02X", config_data[i]);
|
||||
for (int i = 8; i < 13; i++)
|
||||
printf("%02X", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Byte 13: Reserved
|
||||
printf("[Byte 13] Reserved: %02X\n", config_data[13]);
|
||||
|
||||
// Byte 14: I2C_Enable
|
||||
printf("[Byte 14] I2C_Enable: %02X\n", config_data[14]);
|
||||
|
||||
// Byte 15: Reserved
|
||||
printf("[Byte 15] Reserved: %02X\n", config_data[15]);
|
||||
|
||||
// Byte 16: I2C_Address
|
||||
printf("[Byte 16] I2C_Address: 0x%02X (7-bit: 0x%02X)\n", config_data[16],
|
||||
config_data[16] >> 1);
|
||||
|
||||
// Byte 17: Reserved
|
||||
printf("[Byte 17] Reserved: %02X\n", config_data[17]);
|
||||
|
||||
// Byte 18: OTPmode
|
||||
printf("[Byte 18] OTPmode: 0x%02X\n", config_data[18]);
|
||||
|
||||
// Byte 19: ChipMode
|
||||
printf("[Byte 19] ChipMode: 0x%02X ", config_data[19]);
|
||||
if (config_data[19] & 0x01)
|
||||
printf("[I2C_UserExtraAdd] ");
|
||||
if (config_data[19] & 0x02)
|
||||
printf("[TTL_Enable] ");
|
||||
if (config_data[19] & 0x04)
|
||||
printf("[Watchdog_1.3s] ");
|
||||
printf("\n");
|
||||
|
||||
// Bytes 20-51: SlotConfig[0:15] (16 slots × 2 bytes)
|
||||
printf("\n[Bytes 20-51] SlotConfig[0:15]:\n");
|
||||
for (int slot = 0; slot < 16; slot++) {
|
||||
int offset = 20 + (slot * 2);
|
||||
uint8_t slot_config_low = config_data[offset];
|
||||
uint8_t slot_config_high = config_data[offset + 1];
|
||||
uint16_t slot_config = (slot_config_high << 8) | slot_config_low;
|
||||
printf(" Slot %2d [Bytes %2d-%2d]: 0x%02X 0x%02X (", slot, offset,
|
||||
offset + 1, slot_config_low, slot_config_high);
|
||||
|
||||
// Print 16-bit binary representation
|
||||
for (int b = 15; b >= 0; b--)
|
||||
printf("%d", (slot_config >> b) & 1);
|
||||
printf(")");
|
||||
|
||||
bool is_secret = (slot_config & 0x8000) != 0;
|
||||
bool encrypt_read = (slot_config & 0x4000) != 0;
|
||||
if (is_secret)
|
||||
printf(" [Secret]");
|
||||
if (encrypt_read)
|
||||
printf(" [EncryptRead]");
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// Bytes 52-59: Counter[0]
|
||||
printf("\n[Bytes 52-59] Counter[0]: ");
|
||||
for (int i = 52; i < 60; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Bytes 60-67: Counter[1]
|
||||
printf("[Bytes 60-67] Counter[1]: ");
|
||||
for (int i = 60; i < 68; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Bytes 68-83: LastKeyUse[0:15]
|
||||
printf("\n[Bytes 68-83] LastKeyUse[0:15]: ");
|
||||
for (int i = 68; i < 84; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Byte 84: UserExtra
|
||||
printf("\n[Byte 84] UserExtra: 0x%02X\n", config_data[84]);
|
||||
|
||||
// Byte 85: Selector
|
||||
printf("[Byte 85] Selector: 0x%02X\n", config_data[85]);
|
||||
|
||||
// Byte 86: LockValue (Data/OTP Zone Lock)
|
||||
printf("[Byte 86] LockValue (Data/OTP): 0x%02X %s\n", config_data[86],
|
||||
config_data[86] == 0x00 ? "[LOCKED]" : "[UNLOCKED]");
|
||||
|
||||
// Byte 87: LockConfig (Config Zone Lock)
|
||||
printf("[Byte 87] LockConfig: 0x%02X %s\n", config_data[87],
|
||||
config_data[87] == 0x00 ? "[LOCKED]" : "[UNLOCKED]");
|
||||
|
||||
// Bytes 88-89: SlotLocked
|
||||
printf("\n[Bytes 88-89] SlotLocked: %02X %02X\n", config_data[88],
|
||||
config_data[89]);
|
||||
|
||||
// Bytes 90-91: ChipOptions
|
||||
printf("[Bytes 90-91] ChipOptions: ");
|
||||
uint16_t chip_options = (config_data[91] << 8) | config_data[90];
|
||||
printf("0x%04X\n", chip_options);
|
||||
|
||||
// Bytes 92-95: X509format
|
||||
printf("[Bytes 92-95] X509format: ");
|
||||
for (int i = 92; i < 96; i++)
|
||||
printf("%02X ", config_data[i]);
|
||||
printf("\n");
|
||||
|
||||
// Bytes 96-127: KeyConfig[0:15] (16 slots × 2 bytes)
|
||||
printf("\n[Bytes 96-127] KeyConfig[0:15]:\n");
|
||||
for (int slot = 0; slot < 16; slot++) {
|
||||
int offset = 96 + (slot * 2);
|
||||
uint8_t key_config_low = config_data[offset];
|
||||
uint8_t key_config_high = config_data[offset + 1];
|
||||
uint16_t key_config = (key_config_high << 8) | key_config_low;
|
||||
printf(" Slot %2d [Bytes %2d-%2d]: 0x%02X 0x%02X (", slot, offset,
|
||||
offset + 1, key_config_low, key_config_high);
|
||||
|
||||
// Print 16-bit binary representation
|
||||
for (int b = 15; b >= 0; b--)
|
||||
printf("%d", (key_config >> b) & 1);
|
||||
printf(")");
|
||||
|
||||
bool is_private = (key_config & 0x0001) != 0;
|
||||
if (is_private)
|
||||
printf(" [Private]");
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_ATECC, "\n=== End of ATECC608B Configuration ===\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ATECC608B configuration as JSON string (caller must free the returned
|
||||
* string)
|
||||
*/
|
||||
char *atecc608B_get_config_json() {
|
||||
if (!g_atecc_config_valid) {
|
||||
ESP_LOGE(TAG_ATECC,
|
||||
"Config data not valid. Call atecc608B_print_config() first.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t *config = g_atecc_config_data;
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
|
||||
// Add raw hex data
|
||||
char hex_str[385]; // 128 bytes * 3 chars per byte + null
|
||||
char *ptr = hex_str;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
ptr += sprintf(ptr, "%02X ", config[i]);
|
||||
}
|
||||
cJSON_AddStringToObject(root, "raw_hex", hex_str);
|
||||
|
||||
// Serial Number
|
||||
char serial_str[19];
|
||||
sprintf(serial_str, "%02X%02X%02X%02X%02X%02X%02X%02X%02X", config[0],
|
||||
config[1], config[2], config[3], config[8], config[9], config[10],
|
||||
config[11], config[12]);
|
||||
cJSON_AddStringToObject(root, "serial_number", serial_str);
|
||||
|
||||
// Revision
|
||||
char revision_str[12];
|
||||
sprintf(revision_str, "%02X%02X%02X%02X", config[4], config[5], config[6],
|
||||
config[7]);
|
||||
cJSON_AddStringToObject(root, "revision", revision_str);
|
||||
|
||||
// I2C settings
|
||||
cJSON_AddNumberToObject(root, "i2c_enable", config[14]);
|
||||
cJSON_AddNumberToObject(root, "i2c_address", config[16]);
|
||||
|
||||
// Mode settings
|
||||
cJSON_AddNumberToObject(root, "otp_mode", config[18]);
|
||||
cJSON_AddNumberToObject(root, "chip_mode", config[19]);
|
||||
|
||||
// Lock status
|
||||
cJSON *locks = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(locks, "config_locked", config[87] == 0x00);
|
||||
cJSON_AddBoolToObject(locks, "data_otp_locked", config[86] == 0x00);
|
||||
cJSON_AddItemToObject(root, "locks", locks);
|
||||
|
||||
// Slot configurations
|
||||
cJSON *slots = cJSON_CreateArray();
|
||||
for (int slot = 0; slot < 16; slot++) {
|
||||
cJSON *slot_obj = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(slot_obj, "slot", slot);
|
||||
|
||||
int slot_config_offset = 20 + (slot * 2);
|
||||
uint8_t slot_config_low = config[slot_config_offset];
|
||||
uint8_t slot_config_high = config[slot_config_offset + 1];
|
||||
uint16_t slot_config = (slot_config_high << 8) | slot_config_low;
|
||||
|
||||
cJSON *slot_config_arr = cJSON_CreateArray();
|
||||
char slot_config_low_str[5];
|
||||
char slot_config_high_str[5];
|
||||
sprintf(slot_config_low_str, "0x%02X", slot_config_low);
|
||||
sprintf(slot_config_high_str, "0x%02X", slot_config_high);
|
||||
cJSON_AddItemToArray(slot_config_arr,
|
||||
cJSON_CreateString(slot_config_low_str));
|
||||
cJSON_AddItemToArray(slot_config_arr,
|
||||
cJSON_CreateString(slot_config_high_str));
|
||||
cJSON_AddItemToObject(slot_obj, "slot_config", slot_config_arr);
|
||||
|
||||
// Add 16-bit binary representation
|
||||
char slot_config_bin[18];
|
||||
for (int b = 0; b < 16; b++) {
|
||||
slot_config_bin[15 - b] = ((slot_config >> b) & 1) ? '1' : '0';
|
||||
}
|
||||
slot_config_bin[16] = '\0';
|
||||
cJSON_AddStringToObject(slot_obj, "slot_config_binary", slot_config_bin);
|
||||
|
||||
int key_config_offset = 96 + (slot * 2);
|
||||
uint8_t key_config_low = config[key_config_offset];
|
||||
uint8_t key_config_high = config[key_config_offset + 1];
|
||||
uint16_t key_config = (key_config_high << 8) | key_config_low;
|
||||
|
||||
cJSON *key_config_arr = cJSON_CreateArray();
|
||||
char key_config_low_str[5];
|
||||
char key_config_high_str[5];
|
||||
sprintf(key_config_low_str, "0x%02X", key_config_low);
|
||||
sprintf(key_config_high_str, "0x%02X", key_config_high);
|
||||
cJSON_AddItemToArray(key_config_arr,
|
||||
cJSON_CreateString(key_config_low_str));
|
||||
cJSON_AddItemToArray(key_config_arr,
|
||||
cJSON_CreateString(key_config_high_str));
|
||||
cJSON_AddItemToObject(slot_obj, "key_config", key_config_arr);
|
||||
|
||||
// Add 16-bit binary representation
|
||||
char key_config_bin[18];
|
||||
for (int b = 0; b < 16; b++) {
|
||||
key_config_bin[15 - b] = ((key_config >> b) & 1) ? '1' : '0';
|
||||
}
|
||||
key_config_bin[16] = '\0';
|
||||
cJSON_AddStringToObject(slot_obj, "key_config_binary", key_config_bin);
|
||||
|
||||
cJSON_AddBoolToObject(slot_obj, "is_secret", (slot_config & 0x8000) != 0);
|
||||
cJSON_AddBoolToObject(slot_obj, "encrypt_read",
|
||||
(slot_config & 0x4000) != 0);
|
||||
cJSON_AddBoolToObject(slot_obj, "is_private", (key_config & 0x0001) != 0);
|
||||
|
||||
cJSON_AddItemToArray(slots, slot_obj);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "slots", slots);
|
||||
|
||||
// Additional fields
|
||||
cJSON_AddNumberToObject(root, "user_extra", config[84]);
|
||||
cJSON_AddNumberToObject(root, "selector", config[85]);
|
||||
|
||||
char *json_str = cJSON_Print(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
return json_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release ATECC608B resources
|
||||
*/
|
||||
void atecc608B_release() { atcab_release(); }
|
||||
|
||||
#endif // ATECC608B_H
|
||||
306
main/crypto.h
Normal file
306
main/crypto.h
Normal file
@@ -0,0 +1,306 @@
|
||||
#ifndef CRYPTO_H
|
||||
#define CRYPTO_H
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_system.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/ecdsa.h>
|
||||
#include <mbedtls/ecp.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/gcm.h>
|
||||
#include <mbedtls/md.h>
|
||||
#include <mbedtls/pkcs5.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <mbedtls/sha512.h>
|
||||
|
||||
static const char *TAG_CRYPTO = "crypto";
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
// P-256 uses 256 bits = 32 bytes per coordinate
|
||||
const int P256_PRIVATE_KEY_SIZE = 32; // Scalar value
|
||||
const int P256_PUBLIC_KEY_SIZE = 64; // Uncompressed point (x + y)
|
||||
const int P256_COORDINATE_SIZE = 32; // Single coordinate (x or y)
|
||||
const int GCM_TAG_SIZE = 16;
|
||||
const int SALT_SIZE = 16;
|
||||
const int PBKDF2_ITERATIONS = 1000;
|
||||
|
||||
// --- RNG Management ---
|
||||
class RNG {
|
||||
private:
|
||||
mbedtls_entropy_context entropy;
|
||||
mbedtls_ctr_drbg_context ctr_drbg;
|
||||
bool initialized;
|
||||
|
||||
public:
|
||||
RNG() : initialized(false) {}
|
||||
|
||||
~RNG() { cleanup(); }
|
||||
|
||||
int init() {
|
||||
if (initialized)
|
||||
return 0;
|
||||
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
|
||||
const char *pers = "esp32_tang_server";
|
||||
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
|
||||
(const unsigned char *)pers, strlen(pers));
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
initialized = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
if (initialized) {
|
||||
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||
mbedtls_entropy_free(&entropy);
|
||||
initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_ctr_drbg_context *context() { return &ctr_drbg; }
|
||||
};
|
||||
|
||||
// Global RNG instance
|
||||
static RNG global_rng;
|
||||
|
||||
// --- P-256 EC Operations ---
|
||||
class P256 {
|
||||
public:
|
||||
static bool generate_keypair(uint8_t *pub_key, uint8_t *priv_key) {
|
||||
int ret = global_rng.init();
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "RNG init failed: -0x%04x", -ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_ecp_point Q;
|
||||
mbedtls_mpi d;
|
||||
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
mbedtls_mpi_init(&d);
|
||||
|
||||
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "ECP group load failed: -0x%04x", -ret);
|
||||
} else {
|
||||
ret = mbedtls_ecp_gen_keypair(&grp, &d, &Q, mbedtls_ctr_drbg_random,
|
||||
global_rng.context());
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "ECP keypair gen failed: -0x%04x", -ret);
|
||||
}
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&d, priv_key, P256_COORDINATE_SIZE);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "Write private key failed: -0x%04x", -ret);
|
||||
}
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key,
|
||||
P256_COORDINATE_SIZE);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "Write pub key X failed: -0x%04x", -ret);
|
||||
}
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
|
||||
pub_key + P256_COORDINATE_SIZE,
|
||||
P256_COORDINATE_SIZE);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG_CRYPTO, "Write pub key Y failed: -0x%04x", -ret);
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
mbedtls_mpi_free(&d);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
static bool compute_public_key(const uint8_t *priv_key, uint8_t *pub_key) {
|
||||
if (global_rng.init() != 0)
|
||||
return false;
|
||||
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_ecp_point Q;
|
||||
mbedtls_mpi d;
|
||||
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
mbedtls_mpi_init(&d);
|
||||
|
||||
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_mul(&grp, &Q, &d, &grp.G, mbedtls_ctr_drbg_random,
|
||||
global_rng.context());
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
|
||||
pub_key + P256_COORDINATE_SIZE,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
mbedtls_mpi_free(&d);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
static bool ecdh_compute_shared_point(const uint8_t *peer_pub_key,
|
||||
const uint8_t *priv_key,
|
||||
uint8_t *shared_point,
|
||||
bool full_point = true) {
|
||||
if (global_rng.init() != 0)
|
||||
return false;
|
||||
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_ecp_point Q;
|
||||
mbedtls_mpi d;
|
||||
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
mbedtls_mpi_init(&d);
|
||||
|
||||
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(X), peer_pub_key,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(Y),
|
||||
peer_pub_key + P256_COORDINATE_SIZE,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_lset(&Q.MBEDTLS_PRIVATE(Z), 1);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_check_pubkey(&grp, &Q);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_mul(&grp, &Q, &d, &Q, mbedtls_ctr_drbg_random,
|
||||
global_rng.context());
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), shared_point,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0 && full_point) {
|
||||
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
|
||||
shared_point + P256_COORDINATE_SIZE,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
mbedtls_mpi_free(&d);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
static bool sign(const uint8_t *hash, size_t hash_len,
|
||||
const uint8_t *priv_key, uint8_t *signature) {
|
||||
if (global_rng.init() != 0)
|
||||
return false;
|
||||
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_mpi d, r, s;
|
||||
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_mpi_init(&d);
|
||||
mbedtls_mpi_init(&r);
|
||||
mbedtls_mpi_init(&s);
|
||||
|
||||
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecdsa_sign(&grp, &r, &s, &d, hash, hash_len,
|
||||
mbedtls_ctr_drbg_random, global_rng.context());
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&r, signature, P256_COORDINATE_SIZE);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_mpi_write_binary(&s, signature + P256_COORDINATE_SIZE,
|
||||
P256_COORDINATE_SIZE);
|
||||
}
|
||||
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_mpi_free(&d);
|
||||
mbedtls_mpi_free(&r);
|
||||
mbedtls_mpi_free(&s);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
};
|
||||
|
||||
// --- AES-GCM Operations ---
|
||||
class AESGCM {
|
||||
public:
|
||||
static bool encrypt(uint8_t *plaintext, size_t len, const uint8_t *key,
|
||||
size_t key_len, const uint8_t *iv, size_t iv_len,
|
||||
const uint8_t *aad, size_t aad_len, uint8_t *tag) {
|
||||
mbedtls_gcm_context ctx;
|
||||
mbedtls_gcm_init(&ctx);
|
||||
|
||||
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, len, iv,
|
||||
iv_len, aad, aad_len, plaintext,
|
||||
plaintext, GCM_TAG_SIZE, tag);
|
||||
}
|
||||
|
||||
mbedtls_gcm_free(&ctx);
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
static bool decrypt(uint8_t *ciphertext, size_t len, const uint8_t *key,
|
||||
size_t key_len, const uint8_t *iv, size_t iv_len,
|
||||
const uint8_t *aad, size_t aad_len, const uint8_t *tag) {
|
||||
mbedtls_gcm_context ctx;
|
||||
mbedtls_gcm_init(&ctx);
|
||||
|
||||
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_gcm_auth_decrypt(&ctx, len, iv, iv_len, aad, aad_len, tag,
|
||||
GCM_TAG_SIZE, ciphertext, ciphertext);
|
||||
}
|
||||
|
||||
mbedtls_gcm_free(&ctx);
|
||||
return (ret == 0);
|
||||
}
|
||||
};
|
||||
|
||||
// --- PBKDF2 ---
|
||||
class PBKDF2 {
|
||||
public:
|
||||
static int derive_key(uint8_t *output, size_t key_len, const char *password,
|
||||
const uint8_t *salt, size_t salt_len, int iterations) {
|
||||
return mbedtls_pkcs5_pbkdf2_hmac_ext(
|
||||
MBEDTLS_MD_SHA256, (const uint8_t *)password, strlen(password), salt,
|
||||
salt_len, iterations, key_len, output);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // CRYPTO_H
|
||||
30
main/encoding.h
Normal file
30
main/encoding.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef ENCODING_H
|
||||
#define ENCODING_H
|
||||
|
||||
#include <atca_helpers.h>
|
||||
#include <cstring>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <string>
|
||||
|
||||
static bool b64url_encode_buf(const uint8_t *data, size_t data_len,
|
||||
char *out_buf, size_t out_max_len) {
|
||||
size_t b64_len = out_max_len;
|
||||
ATCA_STATUS status = atcab_base64encode_(data, data_len, out_buf, &b64_len,
|
||||
atcab_b64rules_urlsafe());
|
||||
|
||||
return (status == ATCA_SUCCESS && b64_len < out_max_len);
|
||||
}
|
||||
|
||||
static bool b64url_decode_buf(const char *in_str, uint8_t *out_buf,
|
||||
size_t expected_len) {
|
||||
if (!in_str || !out_buf)
|
||||
return false;
|
||||
|
||||
size_t out_len = expected_len;
|
||||
ATCA_STATUS status = atcab_base64decode_(in_str, strlen(in_str), out_buf,
|
||||
&out_len, atcab_b64rules_urlsafe());
|
||||
|
||||
return (status == ATCA_SUCCESS && out_len == expected_len);
|
||||
}
|
||||
|
||||
#endif // ENCODING_H
|
||||
6
main/idf_component.yml
Normal file
6
main/idf_component.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
dependencies:
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
esp-cryptoauthlib:
|
||||
git: https://github.com/espressif/esp-cryptoauthlib.git
|
||||
12
main/main.cpp
Normal file
12
main/main.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* ESP32 Tang Server - Main Entry Point
|
||||
*/
|
||||
#include "TangServer.h"
|
||||
|
||||
extern "C" void app_main(void) {
|
||||
setup();
|
||||
|
||||
while (true) {
|
||||
loop();
|
||||
}
|
||||
}
|
||||
357
main/provision.h
Normal file
357
main/provision.h
Normal file
@@ -0,0 +1,357 @@
|
||||
#ifndef PROVISION_H
|
||||
#define PROVISION_H
|
||||
|
||||
#include "cryptoauthlib.h"
|
||||
#include <esp_efuse.h>
|
||||
#include <esp_efuse_table.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG_PROVISION = "provision";
|
||||
|
||||
// Hardcoded HMAC key for prototyping (32 bytes)
|
||||
// WARNING: This is for prototyping only! In production, this should be securely
|
||||
// generated
|
||||
static const uint8_t IO_KEY[32] = {
|
||||
0x2c, 0x43, 0x34, 0x9c, 0x4c, 0xe6, 0x70, 0xf3, 0xcb, 0x10, 0xef,
|
||||
0xcc, 0x56, 0xf0, 0xd0, 0xc4, 0x03, 0x2c, 0x45, 0x9f, 0xf3, 0xcb,
|
||||
0x29, 0x27, 0x22, 0x8e, 0x93, 0x3c, 0xfe, 0x6e, 0x87, 0xed};
|
||||
|
||||
static const uint8_t HMAC_KEY[32] = {
|
||||
0x24, 0x01, 0x2a, 0xf7, 0x3e, 0x62, 0x7a, 0x5e, 0x5e, 0xdc, 0xf0,
|
||||
0xce, 0xd6, 0xe5, 0x32, 0x20, 0x56, 0xca, 0x29, 0xd1, 0x52, 0xf8,
|
||||
0x17, 0x23, 0x06, 0x75, 0x4f, 0x1d, 0xb9, 0x85, 0x51, 0x5e};
|
||||
|
||||
// ATECC608B Configuration values
|
||||
// SlotConfig (Bytes 20-51): 32 bytes for 16 slots (2 bytes each)
|
||||
static const uint8_t ATECC_SLOT_CONFIG[32] = {
|
||||
0x81, 0x00, 0x81, 0x20, 0x81, 0x20, 0x81, 0x20, 0x84, 0x20, 0x84,
|
||||
0x20, 0xc6, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9a, 0x4a,
|
||||
0x84, 0x20, 0x84, 0x20, 0x9a, 0x4a, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// KeyConfig (Bytes 96-127): 32 bytes for 16 slots (2 bytes each)
|
||||
static const uint8_t ATECC_KEY_CONFIG[32] = {
|
||||
0x33, 0x00, 0x13, 0x00, 0xd3, 0x09, 0x93, 0x0a, 0x53, 0x00, 0x53,
|
||||
0x10, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x5c, 0x00,
|
||||
0x53, 0x00, 0x53, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Byte 69: Persistent Latch enabled with Keyslot 10
|
||||
static const uint8_t ATECC_LAST_KEY_USE_10 = 0x8a;
|
||||
|
||||
// Bytes 90-91: IO Protection optional and Key is Slot 6
|
||||
static const uint8_t ATECC_CHIP_OPTIONS[2] = {0x02, 0x60};
|
||||
|
||||
/**
|
||||
* Check if ATECC608B config zone is locked
|
||||
* Returns: true if locked, false if unlocked
|
||||
*/
|
||||
bool is_atecc608b_config_locked() {
|
||||
extern uint8_t g_atecc_config_data[128];
|
||||
extern bool g_atecc_config_valid;
|
||||
|
||||
// Read config if not already read
|
||||
if (!g_atecc_config_valid) {
|
||||
extern bool atecc608B_read_config();
|
||||
if (!atecc608B_read_config()) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config");
|
||||
return false; // Assume unlocked if we can't read
|
||||
}
|
||||
}
|
||||
|
||||
// Byte 87: LockConfig (Config Zone Lock)
|
||||
// 0x00 = LOCKED, any other value = UNLOCKED
|
||||
bool locked = (g_atecc_config_data[87] == 0x00);
|
||||
ESP_LOGI(TAG_PROVISION, "ATECC608B Config Zone: %s (0x%02X)",
|
||||
locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[87]);
|
||||
return locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ATECC608B data zone is locked
|
||||
* Returns: true if locked, false if unlocked
|
||||
*/
|
||||
bool is_atecc608b_data_locked() {
|
||||
extern uint8_t g_atecc_config_data[128];
|
||||
extern bool g_atecc_config_valid;
|
||||
|
||||
// Read config if not already read
|
||||
if (!g_atecc_config_valid) {
|
||||
extern bool atecc608B_read_config();
|
||||
if (!atecc608B_read_config()) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config");
|
||||
return false; // Assume unlocked if we can't read
|
||||
}
|
||||
}
|
||||
|
||||
// Byte 86: LockValue (Data/OTP Zone Lock)
|
||||
// 0x00 = LOCKED, any other value = UNLOCKED
|
||||
bool locked = (g_atecc_config_data[86] == 0x00);
|
||||
ESP_LOGI(TAG_PROVISION, "ATECC608B Data Zone: %s (0x%02X)",
|
||||
locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[86]);
|
||||
return locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ESP32-C6 efuse BLOCK_KEY5 is already used
|
||||
* Returns: true if used, false if not used
|
||||
*/
|
||||
bool is_efuse_key5_used() {
|
||||
// Read the key purpose for BLOCK_KEY5
|
||||
esp_efuse_purpose_t purpose;
|
||||
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_KEY_PURPOSE_5, &purpose,
|
||||
sizeof(purpose) * 8);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to read KEY_PURPOSE_5: %s",
|
||||
esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "EFUSE KEY_PURPOSE_5: %d (0=NONE/unused)", purpose);
|
||||
|
||||
// Purpose 0 (ESP_EFUSE_KEY_PURPOSE_USER) typically means unused/default
|
||||
// Any non-zero value means it's configured for something
|
||||
return (purpose != ESP_EFUSE_KEY_PURPOSE_USER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision the ATECC608B configuration and lock the config zone
|
||||
* Returns: true on success, false on failure
|
||||
*/
|
||||
bool provision_atecc608b_config() {
|
||||
ESP_LOGI(TAG_PROVISION,
|
||||
"=== Starting ATECC608B Configuration Provisioning ===");
|
||||
|
||||
// Check if config zone is already locked
|
||||
if (is_atecc608b_config_locked()) {
|
||||
ESP_LOGW(TAG_PROVISION, "Config zone is already locked!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wake the device
|
||||
ATCA_STATUS status = atcab_wakeup();
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to wake ATECC608B: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Ensure we have the current 128-byte config read into memory
|
||||
extern uint8_t g_atecc_config_data[128];
|
||||
extern bool g_atecc_config_valid;
|
||||
|
||||
if (!g_atecc_config_valid) {
|
||||
ESP_LOGI(TAG_PROVISION, "Reading full config zone into memory...");
|
||||
status = atcab_read_config_zone(g_atecc_config_data);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to read config zone: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
g_atecc_config_valid = true;
|
||||
}
|
||||
|
||||
// 2. Modify the in-memory array with our desired settings
|
||||
ESP_LOGI(TAG_PROVISION, "Applying configuration changes to memory...");
|
||||
memcpy(&g_atecc_config_data[20], ATECC_SLOT_CONFIG, 32); // SlotConfig
|
||||
g_atecc_config_data[69] = ATECC_LAST_KEY_USE_10; // Persistent Latch
|
||||
memcpy(&g_atecc_config_data[90], ATECC_CHIP_OPTIONS, 2); // ChipOptions
|
||||
memcpy(&g_atecc_config_data[96], ATECC_KEY_CONFIG, 32); // KeyConfig
|
||||
|
||||
// 3. Write the whole modified config zone back to the device
|
||||
// atcab_write_config_zone safely skips the read-only bytes (0-15)
|
||||
ESP_LOGI(TAG_PROVISION, "Writing modified config zone back to ATECC608B...");
|
||||
status = atcab_write_config_zone(g_atecc_config_data);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to write config zone: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG_PROVISION, "Configuration written successfully");
|
||||
|
||||
// 4. Lock the config zone
|
||||
ESP_LOGI(TAG_PROVISION, "Locking config zone...");
|
||||
status = atcab_lock_config_zone();
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to lock config zone: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION,
|
||||
"ATECC608B configuration provisioned and locked successfully!");
|
||||
|
||||
// Invalidate cached config so it gets accurately re-read on next use
|
||||
g_atecc_config_valid = false;
|
||||
|
||||
// Verify it was locked
|
||||
if (is_atecc608b_config_locked()) {
|
||||
ESP_LOGI(TAG_PROVISION, "Verification: Config zone is now locked");
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG_PROVISION,
|
||||
"Verification failed: Config zone still appears unlocked");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision the ATECC608B Data Zone with required keys
|
||||
* Returns: true on success, false on failure
|
||||
*/
|
||||
bool provision_atecc608b_data_zone() {
|
||||
ESP_LOGI(TAG_PROVISION, "=== Starting ATECC608B Data Zone Provisioning ===");
|
||||
|
||||
// 1. Prerequisites check
|
||||
if (is_atecc608b_data_locked()) {
|
||||
ESP_LOGW(TAG_PROVISION,
|
||||
"Data zone is already locked! Cannot provision keys.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_atecc608b_config_locked()) {
|
||||
ESP_LOGE(TAG_PROVISION,
|
||||
"Config zone MUST be locked before generating keys (GenKey).");
|
||||
return false;
|
||||
}
|
||||
|
||||
ATCA_STATUS status;
|
||||
|
||||
// 2. Write Symmetric Keys (Slots 6, 10, 13)
|
||||
ESP_LOGI(TAG_PROVISION, "Writing IO_KEY to Slot 6...");
|
||||
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 6, 0, IO_KEY, 32);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed Slot 6: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 10...");
|
||||
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 10, 0, HMAC_KEY, 32);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed Slot 10: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 13...");
|
||||
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 13, 0, HMAC_KEY, 32);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed Slot 13: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Write ECC Public Key to Slot 9
|
||||
// Note: The 0x04 prefix byte is stripped. The ATECC608B expects exactly 64
|
||||
// bytes.
|
||||
static const uint8_t SLOT9_PUB_KEY[64] = {
|
||||
0x76, 0xc1, 0xa2, 0xe9, 0x63, 0xda, 0x58, 0x41, 0x12, 0x4e, 0xe7,
|
||||
0xc5, 0x3b, 0xeb, 0x2d, 0xad, 0x72, 0xf4, 0xc4, 0x61, 0xb8, 0x4a,
|
||||
0x65, 0xb7, 0xc7, 0x91, 0xdd, 0x59, 0xf9, 0x0a, 0xad, 0xf0, 0x6f,
|
||||
0x13, 0xf2, 0xb6, 0x29, 0x05, 0x4f, 0xab, 0x98, 0xdc, 0xfb, 0x93,
|
||||
0xab, 0xbd, 0x90, 0xd1, 0xea, 0x92, 0x91, 0x0a, 0xfe, 0x95, 0x7c,
|
||||
0xf6, 0xc7, 0x97, 0x41, 0x8e, 0x96, 0x6c, 0xaa, 0x15};
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "Writing ECC Public Key to Slot 9...");
|
||||
status = atcab_write_pubkey(9, SLOT9_PUB_KEY);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed Slot 9: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Generate Random ECC Private Keys (Slots 0-5, 11, 12)
|
||||
uint8_t pubkey[64]; // Buffer to catch the generated public keys
|
||||
const uint8_t genkey_slots[] = {0, 1, 2, 3, 4, 5, 11, 12};
|
||||
|
||||
for (int i = 0; i < sizeof(genkey_slots) / sizeof(genkey_slots[0]); i++) {
|
||||
uint8_t slot = genkey_slots[i];
|
||||
ESP_LOGI(TAG_PROVISION, "Generating ECC Private Key in Slot %d...", slot);
|
||||
|
||||
status = atcab_genkey(slot, pubkey);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed GenKey Slot %d: 0x%02X", slot, status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "Successfully generated Private Key in Slot %d.",
|
||||
slot);
|
||||
ESP_LOGI(TAG_PROVISION, "Corresponding Public Key (64 bytes):");
|
||||
|
||||
// Standard ESP-IDF hex dump (good for reading)
|
||||
ESP_LOG_BUFFER_HEX(TAG_PROVISION, pubkey, sizeof(pubkey));
|
||||
|
||||
// Continuous hex string (good for copy-pasting to host/scripts)
|
||||
printf("--> Copy this public key for Slot %d: 04",
|
||||
slot); // Prepending the 0x04 uncompressed prefix
|
||||
for (int j = 0; j < 64; j++) {
|
||||
printf("%02x", pubkey[j]);
|
||||
}
|
||||
printf("\n\n");
|
||||
}
|
||||
|
||||
// 5. Lock the Data Zone
|
||||
ESP_LOGI(TAG_PROVISION, "Locking Data Zone...");
|
||||
status = atcab_lock_data_zone();
|
||||
if (status != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to lock Data Zone: 0x%02X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION,
|
||||
"ATECC608B Data Zone provisioned and locked successfully!");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provisioning is needed
|
||||
* Returns: true if any condition requires provisioning
|
||||
*/
|
||||
bool needs_provisioning() {
|
||||
bool config_unlocked = !is_atecc608b_config_locked();
|
||||
bool data_unlocked = !is_atecc608b_data_locked();
|
||||
bool key5_unused = !is_efuse_key5_used();
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "Provisioning check:");
|
||||
ESP_LOGI(TAG_PROVISION, " - Config unlocked: %s",
|
||||
config_unlocked ? "YES" : "NO");
|
||||
ESP_LOGI(TAG_PROVISION, " - Data unlocked: %s",
|
||||
data_unlocked ? "YES" : "NO");
|
||||
ESP_LOGI(TAG_PROVISION, " - KEY5 unused: %s", key5_unused ? "YES" : "NO");
|
||||
ESP_LOGI(TAG_PROVISION, " - Needs provisioning: %s",
|
||||
(config_unlocked || data_unlocked || key5_unused) ? "YES" : "NO");
|
||||
|
||||
return (config_unlocked || data_unlocked || key5_unused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision the ESP32-C6 efuse BLOCK_KEY5 with hardcoded HMAC key
|
||||
* Returns: true on success, false on failure
|
||||
*/
|
||||
bool provision_efuse_key5() {
|
||||
ESP_LOGI(TAG_PROVISION, "=== Starting EFUSE KEY5 Provisioning ===");
|
||||
|
||||
// Check if already used
|
||||
if (is_efuse_key5_used()) {
|
||||
ESP_LOGW(TAG_PROVISION, "EFUSE KEY5 is already used!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the HMAC key to BLOCK_KEY5
|
||||
ESP_LOGI(TAG_PROVISION, "Writing HMAC key to BLOCK_KEY5...");
|
||||
esp_err_t err =
|
||||
esp_efuse_write_key(EFUSE_BLK_KEY5, ESP_EFUSE_KEY_PURPOSE_HMAC_UP,
|
||||
HMAC_KEY, sizeof(HMAC_KEY));
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG_PROVISION, "Failed to write EFUSE KEY5: %s",
|
||||
esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_PROVISION, "EFUSE KEY5 provisioned successfully!");
|
||||
ESP_LOGI(TAG_PROVISION, "Purpose set to: HMAC_UP");
|
||||
|
||||
// Verify it was written
|
||||
if (is_efuse_key5_used()) {
|
||||
ESP_LOGI(TAG_PROVISION, "Verification: KEY5 is now marked as used");
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG_PROVISION, "Verification failed: KEY5 still appears unused");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PROVISION_H
|
||||
187
main/provision_handlers.h
Normal file
187
main/provision_handlers.h
Normal file
@@ -0,0 +1,187 @@
|
||||
#ifndef PROVISION_HANDLERS_H
|
||||
#define PROVISION_HANDLERS_H
|
||||
|
||||
#include "provision.h"
|
||||
#include "provision_web_page.h"
|
||||
#include <cJSON.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG_PROVISION_HANDLERS = "provision_handlers";
|
||||
|
||||
// Forward declarations
|
||||
extern httpd_handle_t server_http;
|
||||
|
||||
/**
|
||||
* Serve the provisioning web page
|
||||
*/
|
||||
static esp_err_t handle_provision_page(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_sendstr(req, PROVISION_WEB_PAGE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Get provisioning status
|
||||
*/
|
||||
static esp_err_t handle_provision_status(httpd_req_t *req) {
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
|
||||
bool config_unlocked = !is_atecc608b_config_locked();
|
||||
bool data_unlocked = !is_atecc608b_data_locked();
|
||||
bool key5_unused = !is_efuse_key5_used();
|
||||
bool needs_prov = needs_provisioning();
|
||||
|
||||
cJSON_AddBoolToObject(response, "needs_provisioning", needs_prov);
|
||||
cJSON_AddBoolToObject(response, "config_unlocked", config_unlocked);
|
||||
cJSON_AddBoolToObject(response, "data_unlocked", data_unlocked);
|
||||
cJSON_AddBoolToObject(response, "key5_unused", key5_unused);
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_sendstr(req, json_str);
|
||||
free(json_str);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Provision the device EFUSE
|
||||
*/
|
||||
static esp_err_t handle_provision_api(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning API called");
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
bool success = false;
|
||||
const char *message = "Unknown error";
|
||||
|
||||
// Check if provisioning is actually needed
|
||||
if (!needs_provisioning()) {
|
||||
message = "Device is already provisioned";
|
||||
success = true;
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message);
|
||||
} else {
|
||||
bool atecc_done = true;
|
||||
bool efuse_done = true;
|
||||
|
||||
// Step 1: Provision ATECC608B config if needed
|
||||
if (!is_atecc608b_config_locked()) {
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS,
|
||||
"Provisioning ATECC608B configuration...");
|
||||
atecc_done = provision_atecc608b_config();
|
||||
if (!atecc_done) {
|
||||
message = "Failed to provision ATECC608B configuration";
|
||||
success = false;
|
||||
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Provision EFUSE KEY5 if needed and previous step succeeded
|
||||
if (atecc_done && !is_efuse_key5_used()) {
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning EFUSE KEY5...");
|
||||
efuse_done = provision_efuse_key5();
|
||||
if (!efuse_done) {
|
||||
message = "Failed to provision EFUSE KEY5";
|
||||
success = false;
|
||||
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_atecc608b_data_locked()) {
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning ATECC608B data zone...");
|
||||
bool data_done = provision_atecc608b_data_zone();
|
||||
if (!data_done) {
|
||||
message = "Failed to provision ATECC608B data zone";
|
||||
success = false;
|
||||
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
// Build success message
|
||||
if (atecc_done && efuse_done) {
|
||||
if (!is_atecc608b_config_locked() && !is_efuse_key5_used()) {
|
||||
message =
|
||||
"Provisioned ATECC608B configuration and EFUSE KEY5 successfully";
|
||||
} else if (!is_atecc608b_config_locked()) {
|
||||
message = "ATECC608B configuration provisioned and locked successfully";
|
||||
} else {
|
||||
message = "EFUSE KEY5 provisioned successfully with HMAC key";
|
||||
}
|
||||
success = true;
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
// Build JSON response
|
||||
cJSON_AddBoolToObject(response, "success", success);
|
||||
cJSON_AddStringToObject(response, "message", message);
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
// Send response
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_status(req, success ? "200 OK" : "500 Internal Server Error");
|
||||
httpd_resp_sendstr(req, json_str);
|
||||
free(json_str);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CORS preflight for provision API
|
||||
*/
|
||||
static esp_err_t handle_provision_options(httpd_req_t *req) {
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
||||
httpd_resp_set_status(req, "204 No Content");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register provisioning handlers
|
||||
*/
|
||||
void register_provision_handlers(httpd_handle_t server) {
|
||||
// Provision page
|
||||
httpd_uri_t provision_page_uri = {.uri = "/provision",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_provision_page,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &provision_page_uri);
|
||||
|
||||
// API endpoint for getting status
|
||||
httpd_uri_t provision_status_uri = {.uri = "/api/provision/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_provision_status,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &provision_status_uri);
|
||||
|
||||
// API endpoint for provisioning
|
||||
httpd_uri_t provision_api_uri = {.uri = "/api/provision",
|
||||
.method = HTTP_POST,
|
||||
.handler = handle_provision_api,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &provision_api_uri);
|
||||
|
||||
// CORS preflight
|
||||
httpd_uri_t provision_options_uri = {.uri = "/api/provision",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = handle_provision_options,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &provision_options_uri);
|
||||
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provision routes registered:");
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS,
|
||||
" GET /provision - Provision page");
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS,
|
||||
" GET /api/provision/status - Provision status");
|
||||
ESP_LOGI(TAG_PROVISION_HANDLERS, " POST /api/provision - Provision API");
|
||||
}
|
||||
|
||||
#endif // PROVISION_HANDLERS_H
|
||||
323
main/provision_web_page.h
Normal file
323
main/provision_web_page.h
Normal file
@@ -0,0 +1,323 @@
|
||||
#ifndef PROVISION_WEB_PAGE_H
|
||||
#define PROVISION_WEB_PAGE_H
|
||||
|
||||
// Embedded web interface for device provisioning
|
||||
|
||||
const char PROVISION_WEB_PAGE[] = R"rawliteral(
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 Provisioning</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border: 2px solid #ffc107;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.warning-box h3 {
|
||||
color: #856404;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.warning-box ul {
|
||||
margin-left: 20px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.warning-box li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e7f3ff;
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
color: #1976D2;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
color: #1565C0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.provision-btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.provision-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.provision-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.provision-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background: #d4edda;
|
||||
border: 2px solid #28a745;
|
||||
color: #155724;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background: #f8d7da;
|
||||
border: 2px solid #dc3545;
|
||||
color: #721c24;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-message.info {
|
||||
background: #d1ecf1;
|
||||
border: 2px solid #17a2b8;
|
||||
color: #0c5460;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.skip-link a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.skip-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔧 Device Provisioning Required</h1>
|
||||
<p>Your device needs to be provisioned before first use</p>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<h3>⚠️ Provisioning Needed</h3>
|
||||
<ul id="provisionReasons">
|
||||
<li>Loading status...</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>ℹ️ What This Does</h3>
|
||||
<p>
|
||||
This will provision your device security hardware:
|
||||
</p>
|
||||
<ul style="margin-top: 10px; margin-left: 20px; color: #1565C0;">
|
||||
<li><strong>ATECC608B:</strong> Write secure configuration (SlotConfig, KeyConfig, ChipOptions) and lock the config zone</li>
|
||||
<li><strong>ESP32-C6:</strong> Write hardcoded HMAC key to EFUSE BLOCK_KEY5 with purpose <code>EFUSE_KEY_PURPOSE_HMAC_UP</code></li>
|
||||
</ul>
|
||||
<p style="margin-top: 10px; font-weight: bold;">
|
||||
⚠️ Warning: These are one-time operations and cannot be undone!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="statusMessage" class="status-message"></div>
|
||||
Device
|
||||
<button id="provisionBtn" class="provision-btn" onclick="provisionDevice()">
|
||||
Provision EFUSE KEY5
|
||||
</button>
|
||||
|
||||
<div class="skip-link">
|
||||
<a href="/">Skip and continue anyway</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Fetch and display current provisioning status on page load
|
||||
async function loadProvisionStatus() {
|
||||
const reasonsList = document.getElementById('provisionReasons');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/provision/status');
|
||||
const status = await response.json();
|
||||
|
||||
// Clear loading message
|
||||
reasonsList.innerHTML = '';
|
||||
|
||||
// Build list of actual conditions
|
||||
|
||||
if (status.config_unlocked) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = 'ATECC608B Config Zone is unlocked';
|
||||
reasonsList.appendChild(li);
|
||||
}
|
||||
|
||||
if (status.data_unlocked) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = 'ATECC608B Data Zone is unlocked';
|
||||
reasonsList.appendChild(li);
|
||||
}
|
||||
|
||||
if (status.key5_unused) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = 'ESP32-C6 EFUSE BLOCK_KEY5 is not configured';
|
||||
reasonsList.appendChild(li);
|
||||
}
|
||||
|
||||
// If somehow no conditions are true, show message
|
||||
if (!status.needs_provisioning) {
|
||||
reasonsList.innerHTML = '<li>No provisioning needed (you may skip)</li>';
|
||||
}
|
||||
} catch (error) {
|
||||
reasonsList.innerHTML = '<li>Error loading status: ' + error.message + '</li>';
|
||||
}
|
||||
}
|
||||
|
||||
async function provisionDevice() {
|
||||
const btn = document.getElementById('provisionBtn');
|
||||
const statusMsg = document.getElementById('statusMessage');
|
||||
|
||||
// Disable button
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Provisioning...';
|
||||
|
||||
// Show info messageProvisioning device security hardware
|
||||
statusMsg.className = 'status-message info';
|
||||
statusMsg.textContent = 'Writing HMAC key to EFUSE BLOCK_KEY5...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/provision', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
statusMsg.className = 'status-message success';
|
||||
statusMsg.textContent = '✓ ' + result.message;
|
||||
btn.textContent = 'Provisioning Complete!';
|
||||
|
||||
// Redirect to home after 2 seconds
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
} else {
|
||||
statusMsg.className = 'status-message error';
|
||||
statusMsg.textContent = '✗ Error: ' + (result.message || 'Unknown error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Retry Provisioning';
|
||||
}
|
||||
} catch (error) {
|
||||
statusMsg.className = 'status-message error';
|
||||
statusMsg.textContent = '✗ Connection error: ' + error.message;
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Retry Provisioning';
|
||||
}
|
||||
}
|
||||
|
||||
// Load status when page loads
|
||||
window.addEventListener('DOMContentLoaded', loadProvisionStatus);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)rawliteral";
|
||||
|
||||
#endif // PROVISION_WEB_PAGE_H
|
||||
288
main/tang_handlers.h
Normal file
288
main/tang_handlers.h
Normal file
@@ -0,0 +1,288 @@
|
||||
#ifndef TANG_HANDLERS_H
|
||||
#define TANG_HANDLERS_H
|
||||
|
||||
#include "crypto.h"
|
||||
#include "cryptoauthlib.h"
|
||||
#include "encoding.h"
|
||||
#include "tang_storage.h"
|
||||
#include <cJSON.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG_HANDLERS = "tang_handlers";
|
||||
|
||||
// Forward declarations
|
||||
extern httpd_handle_t server_http;
|
||||
extern TangKeyStore keystore;
|
||||
extern bool unlocked;
|
||||
|
||||
// GET /adv - Advertisement endpoint (signed JWK set)
|
||||
static esp_err_t handle_adv(httpd_req_t *req) {
|
||||
if (!unlocked) {
|
||||
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||
httpd_resp_sendstr(req, "Server not active");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint8_t sig_pub[ATCA_PUB_KEY_SIZE];
|
||||
|
||||
if (atcab_get_pubkey(1, sig_pub) != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_HANDLERS, "Failed to read public keys from secure element");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Hardware error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char sig_x_b64[64] = {0}, sig_y_b64[64] = {0};
|
||||
char rec_x_b64[64] = {0}, rec_y_b64[64] = {0};
|
||||
|
||||
b64url_encode_buf(&sig_pub[0], 32, sig_x_b64, sizeof(sig_x_b64));
|
||||
b64url_encode_buf(&sig_pub[32], 32, sig_y_b64, sizeof(sig_y_b64));
|
||||
b64url_encode_buf(&keystore.exc_pub[0], 32, rec_x_b64, sizeof(rec_x_b64));
|
||||
b64url_encode_buf(&keystore.exc_pub[32], 32, rec_y_b64, sizeof(rec_y_b64));
|
||||
|
||||
// Build JWK set payload using cJSON
|
||||
cJSON *payload_root = cJSON_CreateObject();
|
||||
cJSON *keys = cJSON_CreateArray();
|
||||
|
||||
// Signing/verification key
|
||||
cJSON *sig_key = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(sig_key, "kty", "EC");
|
||||
cJSON_AddStringToObject(sig_key, "alg", "ES256");
|
||||
cJSON_AddStringToObject(sig_key, "crv", "P-256");
|
||||
cJSON_AddStringToObject(sig_key, "x", sig_x_b64);
|
||||
cJSON_AddStringToObject(sig_key, "y", sig_y_b64);
|
||||
|
||||
cJSON *sig_key_ops = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(sig_key_ops, cJSON_CreateString("verify"));
|
||||
cJSON_AddItemToObject(sig_key, "key_ops", sig_key_ops);
|
||||
cJSON_AddItemToArray(keys, sig_key);
|
||||
|
||||
cJSON *rec_key = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(rec_key, "alg", "ECMR");
|
||||
cJSON_AddStringToObject(rec_key, "kty", "EC");
|
||||
cJSON_AddStringToObject(rec_key, "crv", "P-256");
|
||||
cJSON_AddStringToObject(rec_key, "x", rec_x_b64);
|
||||
cJSON_AddStringToObject(rec_key, "y", rec_y_b64);
|
||||
cJSON *rec_key_ops = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(rec_key_ops, cJSON_CreateString("deriveKey"));
|
||||
cJSON_AddItemToObject(rec_key, "key_ops", rec_key_ops);
|
||||
cJSON_AddItemToArray(keys, rec_key);
|
||||
|
||||
cJSON_AddItemToObject(payload_root, "keys", keys);
|
||||
|
||||
// Print payload JSON and encode to B64 dynamically
|
||||
char *payload_json = cJSON_PrintUnformatted(payload_root);
|
||||
size_t payload_len = strlen(payload_json);
|
||||
size_t payload_b64_size = ((payload_len + 2) / 3) * 4 + 1;
|
||||
char *payload_b64 = (char *)malloc(payload_b64_size);
|
||||
b64url_encode_buf((uint8_t *)payload_json, payload_len, payload_b64,
|
||||
payload_b64_size);
|
||||
|
||||
free(payload_json);
|
||||
cJSON_Delete(payload_root);
|
||||
|
||||
// Create protected header
|
||||
cJSON *protected_root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(protected_root, "alg", "ES256");
|
||||
cJSON_AddStringToObject(protected_root, "cty", "jwk-set+json");
|
||||
|
||||
char *protected_json = cJSON_PrintUnformatted(protected_root);
|
||||
size_t protected_len = strlen(protected_json);
|
||||
size_t protected_b64_size = ((protected_len + 2) / 3) * 4 + 1;
|
||||
char *protected_b64 = (char *)malloc(protected_b64_size);
|
||||
b64url_encode_buf((uint8_t *)protected_json, protected_len, protected_b64,
|
||||
protected_b64_size);
|
||||
|
||||
free(protected_json);
|
||||
cJSON_Delete(protected_root);
|
||||
|
||||
// Sign the payload
|
||||
size_t signing_input_size =
|
||||
strlen(protected_b64) + 1 + strlen(payload_b64) + 1;
|
||||
char *signing_input = (char *)malloc(signing_input_size);
|
||||
snprintf(signing_input, signing_input_size, "%s.%s", protected_b64,
|
||||
payload_b64);
|
||||
|
||||
uint8_t hash[32];
|
||||
mbedtls_sha256((const uint8_t *)signing_input, strlen(signing_input), hash,
|
||||
0);
|
||||
free(signing_input);
|
||||
|
||||
uint8_t signature[ATCA_ECCP256_SIG_SIZE];
|
||||
if (atcab_sign(1, hash, signature) != ATCA_SUCCESS) {
|
||||
ESP_LOGE(TAG_HANDLERS, "Hardware signing failed");
|
||||
free(payload_b64);
|
||||
free(protected_b64);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Signing failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char sig_b64[128] = {0};
|
||||
b64url_encode_buf(signature, sizeof(signature), sig_b64, sizeof(sig_b64));
|
||||
|
||||
cJSON *jws_root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(jws_root, "payload", payload_b64);
|
||||
cJSON_AddStringToObject(jws_root, "protected", protected_b64);
|
||||
cJSON_AddStringToObject(jws_root, "signature", sig_b64);
|
||||
|
||||
char *response = cJSON_PrintUnformatted(jws_root);
|
||||
cJSON_Delete(jws_root);
|
||||
|
||||
free(payload_b64);
|
||||
free(protected_b64);
|
||||
|
||||
httpd_resp_set_type(
|
||||
req, "application/jose+json"); // Tang expects this specific type
|
||||
httpd_resp_sendstr(req, response);
|
||||
free(response);
|
||||
|
||||
ESP_LOGI(TAG_HANDLERS, "Served /adv");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// POST /rec or /rec/{kid} - Recovery endpoint
|
||||
static esp_err_t handle_rec(httpd_req_t *req) {
|
||||
if (!unlocked) {
|
||||
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||
httpd_resp_sendstr(req, "Server not active");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Read request body
|
||||
char *buf = (char *)malloc(req->content_len + 1);
|
||||
if (!buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Memory allocation failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int ret = httpd_req_recv(req, buf, req->content_len);
|
||||
if (ret <= 0) {
|
||||
free(buf);
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
// Parse JSON
|
||||
cJSON *req_doc = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
|
||||
if (!req_doc) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Extract client's ephemeral public key
|
||||
cJSON *x_item = cJSON_GetObjectItem(req_doc, "x");
|
||||
cJSON *y_item = cJSON_GetObjectItem(req_doc, "y");
|
||||
|
||||
if (!x_item || !y_item || !cJSON_IsString(x_item) ||
|
||||
!cJSON_IsString(y_item)) {
|
||||
cJSON_Delete(req_doc);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
"Missing x or y coordinates");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint8_t client_pub_key[P256_PUBLIC_KEY_SIZE];
|
||||
|
||||
// Use the new helper to decode and strictly validate the length of both
|
||||
// coordinates
|
||||
if (!b64url_decode_buf(x_item->valuestring, &client_pub_key[0],
|
||||
P256_COORDINATE_SIZE) ||
|
||||
!b64url_decode_buf(y_item->valuestring,
|
||||
&client_pub_key[P256_COORDINATE_SIZE],
|
||||
P256_COORDINATE_SIZE)) {
|
||||
cJSON_Delete(req_doc);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid key coordinates");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON_Delete(req_doc);
|
||||
|
||||
// Perform ECDH to get shared point
|
||||
uint8_t shared_point[P256_PUBLIC_KEY_SIZE];
|
||||
if (!P256::ecdh_compute_shared_point(client_pub_key, keystore.exc_priv,
|
||||
shared_point, true)) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"ECDH computation failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char shared_x_b64[64] = {0};
|
||||
char shared_y_b64[64] = {0};
|
||||
|
||||
b64url_encode_buf(&shared_point[0], P256_COORDINATE_SIZE, shared_x_b64,
|
||||
sizeof(shared_x_b64));
|
||||
b64url_encode_buf(&shared_point[P256_COORDINATE_SIZE], P256_COORDINATE_SIZE,
|
||||
shared_y_b64, sizeof(shared_y_b64));
|
||||
|
||||
// Return shared point as JWK
|
||||
cJSON *resp_root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(resp_root, "alg", "ECMR");
|
||||
cJSON_AddStringToObject(resp_root, "kty", "EC");
|
||||
cJSON_AddStringToObject(resp_root, "crv", "P-256");
|
||||
cJSON_AddStringToObject(resp_root, "x", shared_x_b64);
|
||||
cJSON_AddStringToObject(resp_root, "y", shared_y_b64);
|
||||
cJSON *key_ops = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(key_ops, cJSON_CreateString("deriveKey"));
|
||||
cJSON_AddItemToObject(resp_root, "key_ops", key_ops);
|
||||
|
||||
char *response = cJSON_PrintUnformatted(resp_root);
|
||||
cJSON_Delete(resp_root);
|
||||
|
||||
httpd_resp_set_type(req, "application/jose+json");
|
||||
httpd_resp_sendstr(req, response);
|
||||
free(response);
|
||||
|
||||
ESP_LOGI(TAG_HANDLERS, "Served %s", req->uri);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// --- Administration Handlers ---
|
||||
|
||||
// GET /config - Get ATECC608B configuration
|
||||
static esp_err_t handle_config(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG_HANDLERS, "Serving ATECC608B config");
|
||||
|
||||
char *json_str = atecc608B_get_config_json();
|
||||
|
||||
if (!json_str) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Config data not available");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr(req, json_str);
|
||||
free(json_str);
|
||||
|
||||
ESP_LOGI(TAG_HANDLERS, "Served /config");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// GET /reboot - Reboot device
|
||||
static esp_err_t handle_reboot(httpd_req_t *req) {
|
||||
httpd_resp_sendstr(req, "Rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 404 handler - Handle /rec/{kid} by routing to handle_rec
|
||||
static esp_err_t handle_not_found(httpd_req_t *req, httpd_err_code_t err) {
|
||||
// Check if it's a POST to /rec/{kid}
|
||||
if (req->method == HTTP_POST && strncmp(req->uri, "/rec/", 5) == 0) {
|
||||
return handle_rec(req);
|
||||
}
|
||||
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not found");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
#endif // TANG_HANDLERS_H
|
||||
87
main/tang_storage.h
Normal file
87
main/tang_storage.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#ifndef TANG_STORAGE_H
|
||||
#define TANG_STORAGE_H
|
||||
|
||||
#include "crypto.h"
|
||||
#include "encoding.h"
|
||||
#include <cstring>
|
||||
#include <esp_log.h>
|
||||
#include <esp_random.h>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
static const char *TAG_STORAGE = "tang_storage";
|
||||
|
||||
// --- Key Storage & Management ---
|
||||
class TangKeyStore {
|
||||
private:
|
||||
// No longer need salt for password-based encryption
|
||||
|
||||
public:
|
||||
// Tang server keys (encrypted at rest, decrypted in memory when active)
|
||||
uint8_t exc_priv[P256_PRIVATE_KEY_SIZE];
|
||||
uint8_t exc_pub[P256_PUBLIC_KEY_SIZE];
|
||||
|
||||
// Admin key (persistent)
|
||||
uint8_t admin_priv[P256_PRIVATE_KEY_SIZE];
|
||||
uint8_t admin_pub[P256_PUBLIC_KEY_SIZE];
|
||||
|
||||
bool is_configured() {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t required_size = 0;
|
||||
err = nvs_get_blob(handle, "admin_key", nullptr, &required_size);
|
||||
bool configured = (err == ESP_OK && required_size == P256_PRIVATE_KEY_SIZE);
|
||||
|
||||
nvs_close(handle);
|
||||
return configured;
|
||||
}
|
||||
|
||||
// Save Tang keys directly to NVS (no encryption)
|
||||
bool save_tang_keys() {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save exchange key
|
||||
err = nvs_set_blob(handle, "tang_exc_key", exc_priv, P256_PRIVATE_KEY_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
nvs_close(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
return (err == ESP_OK);
|
||||
}
|
||||
|
||||
// Load Tang keys directly from NVS (no decryption)
|
||||
bool load_tang_keys() {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open("tang-server", NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load exchange key
|
||||
size_t len = P256_PRIVATE_KEY_SIZE;
|
||||
err = nvs_get_blob(handle, "tang_exc_key", exc_priv, &len);
|
||||
if (err != ESP_OK || len != P256_PRIVATE_KEY_SIZE) {
|
||||
nvs_close(handle);
|
||||
return false;
|
||||
}
|
||||
P256::compute_public_key(exc_priv, exc_pub);
|
||||
|
||||
nvs_close(handle);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TANG_STORAGE_H
|
||||
535
main/zk_auth.h
Normal file
535
main/zk_auth.h
Normal file
@@ -0,0 +1,535 @@
|
||||
#ifndef ZK_AUTH_H
|
||||
#define ZK_AUTH_H
|
||||
|
||||
#include "cryptoauthlib.h"
|
||||
#include "host/atca_host.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
#include <cJSON.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_system.h>
|
||||
#include <mbedtls/aes.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/ecdh.h>
|
||||
#include <mbedtls/ecp.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/md.h>
|
||||
#include <mbedtls/pkcs5.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Zero-Knowledge Authentication Module
|
||||
// Implements Client-Side KDF + ECIES Tunnel for ESP32-C6
|
||||
// Format: SHA-256 compatible with ATECC608B
|
||||
//
|
||||
// Encryption: AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC)
|
||||
// - Provides confidentiality (CBC) + authenticity (HMAC)
|
||||
// - Blob format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80
|
||||
// bytes
|
||||
// - HMAC verified BEFORE decryption (prevents padding oracle attacks)
|
||||
|
||||
class ZKAuth {
|
||||
private:
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_mpi device_private_d;
|
||||
mbedtls_ecp_point device_public_Q;
|
||||
mbedtls_entropy_context entropy;
|
||||
mbedtls_ctr_drbg_context ctr_drbg;
|
||||
|
||||
uint8_t device_public_key[65]; // Uncompressed: 0x04 + X(32) + Y(32)
|
||||
uint8_t stored_password_hash[32]; // PBKDF2 hash of the correct password
|
||||
|
||||
bool initialized;
|
||||
bool password_set;
|
||||
bool unlocked = false; // Set to true after successful authentication
|
||||
|
||||
// Convert binary to hex string
|
||||
void bin_to_hex(const uint8_t *bin, size_t bin_len, char *hex) {
|
||||
for (size_t i = 0; i < bin_len; i++) {
|
||||
sprintf(hex + (i * 2), "%02x", bin[i]);
|
||||
}
|
||||
hex[bin_len * 2] = '\0';
|
||||
}
|
||||
|
||||
// Convert hex string to binary
|
||||
bool hex_to_bin(const char *hex, uint8_t *bin, size_t bin_len) {
|
||||
if (strlen(hex) != bin_len * 2)
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < bin_len; i++) {
|
||||
if (sscanf(hex + (i * 2), "%2hhx", &bin[i]) != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
ZKAuth() : initialized(false), password_set(false), unlocked(false) {
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_mpi_init(&device_private_d);
|
||||
mbedtls_ecp_point_init(&device_public_Q);
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
}
|
||||
|
||||
~ZKAuth() {
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_mpi_free(&device_private_d);
|
||||
mbedtls_ecp_point_free(&device_public_Q);
|
||||
mbedtls_entropy_free(&entropy);
|
||||
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||
}
|
||||
|
||||
// Initialize the ZK authentication system
|
||||
bool init() {
|
||||
if (initialized)
|
||||
return true;
|
||||
|
||||
// Seed the random number generator
|
||||
const char *pers = "zk_auth_esp32";
|
||||
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
|
||||
(const unsigned char *)pers, strlen(pers));
|
||||
if (ret != 0) {
|
||||
printf("mbedtls_ctr_drbg_seed failed: -0x%04x\n", -ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup ECC group for NIST P-256 (secp256r1)
|
||||
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
|
||||
if (ret != 0) {
|
||||
printf("mbedtls_ecp_group_load failed: -0x%04x\n", -ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate device keypair
|
||||
ret = mbedtls_ecdh_gen_public(&grp, &device_private_d, &device_public_Q,
|
||||
mbedtls_ctr_drbg_random, &ctr_drbg);
|
||||
if (ret != 0) {
|
||||
printf("mbedtls_ecdh_gen_public failed: -0x%04x\n", -ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Export public key to uncompressed format (0x04 + X + Y)
|
||||
size_t olen;
|
||||
ret = mbedtls_ecp_point_write_binary(
|
||||
&grp, &device_public_Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen,
|
||||
device_public_key, sizeof(device_public_key));
|
||||
if (ret != 0 || olen != 65) {
|
||||
printf("mbedtls_ecp_point_write_binary failed: -0x%04x\n", -ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
|
||||
printf("\n=== ZK Authentication Initialized ===\n");
|
||||
printf("Public Key: ");
|
||||
for (int i = 0; i < 65; i++) {
|
||||
printf("%02x", device_public_key[i]);
|
||||
}
|
||||
printf("\n");
|
||||
printf("=====================================\n\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get device identity (for /api/identity endpoint)
|
||||
char *get_identity_json() {
|
||||
char pubkey_hex[131]; // 65 bytes * 2 + null
|
||||
bin_to_hex(device_public_key, 65, pubkey_hex);
|
||||
|
||||
// Get MAC address to use as salt
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
char mac_hex[13]; // 6 bytes * 2 + null
|
||||
bin_to_hex(mac, 6, mac_hex);
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "pubKey", pubkey_hex);
|
||||
cJSON_AddStringToObject(root, "macAddress", mac_hex);
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return json_str;
|
||||
}
|
||||
|
||||
void my_host_check_mac(const uint8_t *target_key, const uint8_t *tempkey,
|
||||
const uint8_t *other_data, const uint8_t *sn,
|
||||
uint8_t *out_mac) {
|
||||
// See section 11.2 of the ATECC608B datasheet CheckMac message format
|
||||
uint8_t msg[88] = {0};
|
||||
memcpy(&msg[0], target_key, 32);
|
||||
memcpy(&msg[32], tempkey, 32);
|
||||
memcpy(&msg[64], &other_data[0], 4);
|
||||
memset(&msg[68], 0, 8);
|
||||
memcpy(&msg[76], &other_data[4], 3);
|
||||
msg[79] = sn[8];
|
||||
memcpy(&msg[80], &other_data[7], 4);
|
||||
msg[84] = sn[0];
|
||||
msg[85] = sn[1];
|
||||
memcpy(&msg[86], &other_data[11], 2);
|
||||
// Hash the exact 88 bytes using the ESP32's hardware accelerator
|
||||
mbedtls_sha256(msg, sizeof(msg), out_mac, 0);
|
||||
}
|
||||
|
||||
bool verify_key(const uint8_t *received_key, size_t key_len) {
|
||||
(void)key_len;
|
||||
if (received_key == NULL)
|
||||
return false;
|
||||
|
||||
ATCA_STATUS status;
|
||||
|
||||
bool latch_status = false;
|
||||
status = atcab_info_get_latch(&latch_status);
|
||||
|
||||
printf("Device Latch Status: %s (0x%02X)\n",
|
||||
latch_status ? "LATCHED" : "UNLATCHED", latch_status);
|
||||
|
||||
uint8_t sn[ATCA_SERIAL_NUM_SIZE] = {0};
|
||||
status = atcab_read_serial_number(sn);
|
||||
if (status != ATCA_SUCCESS) {
|
||||
printf("Failed to read Serial Number\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t nonce_num_in[20] = {0xAA, 0xBB, 0xCC, 0xDD};
|
||||
uint8_t rand_out[32] = {0};
|
||||
uint8_t slot10_mac[32] = {0};
|
||||
uint8_t other_data_10[13] = "TangActivate";
|
||||
struct atca_temp_key temp_key;
|
||||
memset(&temp_key, 0, sizeof(temp_key));
|
||||
|
||||
status = atcab_nonce_rand(nonce_num_in, rand_out);
|
||||
|
||||
// Print the nonce output for debugging
|
||||
if (status != ATCA_SUCCESS) {
|
||||
printf("Slot 10 Nonce failed: 0x%02X\n", status);
|
||||
} else {
|
||||
// Compute the TempKey on the ESP32
|
||||
struct atca_nonce_in_out nonce_params;
|
||||
memset(&nonce_params, 0, sizeof(nonce_params));
|
||||
nonce_params.mode = 0x00;
|
||||
nonce_params.num_in = nonce_num_in;
|
||||
nonce_params.rand_out = rand_out;
|
||||
nonce_params.temp_key = &temp_key;
|
||||
|
||||
atcah_nonce(&nonce_params);
|
||||
|
||||
// Host calculates MAC using the calculated TempKey as the challenge
|
||||
my_host_check_mac(received_key, temp_key.value, other_data_10, sn,
|
||||
slot10_mac);
|
||||
|
||||
// See section 11.2 of the ATECC608B datasheet CheckMac message format
|
||||
status = atcab_checkmac(0x01, 10, NULL, slot10_mac, other_data_10);
|
||||
if (status == ATCA_SUCCESS) {
|
||||
printf("Slot 10 CheckMac: PASSED\n");
|
||||
} else if (status == (ATCA_STATUS)ATCA_CHECKMAC_VERIFY_FAILED) {
|
||||
// 0xD1 (-47 / 0xFFFFFFD1) see atca_status.h
|
||||
printf("Slot 10 CheckMac: ACCESS DENIED (Wrong Password)\n");
|
||||
} else {
|
||||
printf("Slot 10 CheckMac: HARDWARE ERROR (0x%02X)\n", status);
|
||||
}
|
||||
}
|
||||
status = atcab_info_set_latch(true);
|
||||
|
||||
if (status != ATCA_SUCCESS) {
|
||||
printf("Failed to latch device: 0x%02X\n", status);
|
||||
}
|
||||
|
||||
atcab_info_get_latch(&latch_status);
|
||||
printf("Device Latch Status after setting: %s (0x%02X)\n",
|
||||
latch_status ? "LATCHED" : "UNLATCHED", latch_status);
|
||||
|
||||
return (status == ATCA_SUCCESS);
|
||||
}
|
||||
|
||||
// Process unlock request with ECIES tunnel
|
||||
char *process_unlock(const char *json_payload, bool *success_out) {
|
||||
*success_out = false;
|
||||
|
||||
if (!initialized) {
|
||||
return strdup("{\"error\":\"Not initialized\"}");
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
cJSON *doc = cJSON_Parse(json_payload);
|
||||
if (doc == NULL) {
|
||||
const char *error_ptr = cJSON_GetErrorPtr();
|
||||
if (error_ptr != NULL) {
|
||||
printf("JSON parse error before: %s\n", error_ptr);
|
||||
}
|
||||
return strdup("{\"error\":\"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
cJSON *client_pub_item = cJSON_GetObjectItem(doc, "clientPub");
|
||||
cJSON *encrypted_blob_item = cJSON_GetObjectItem(doc, "blob");
|
||||
|
||||
const char *client_pub_hex = NULL;
|
||||
const char *encrypted_blob_hex = NULL;
|
||||
|
||||
if (cJSON_IsString(client_pub_item))
|
||||
client_pub_hex = client_pub_item->valuestring;
|
||||
if (cJSON_IsString(encrypted_blob_item))
|
||||
encrypted_blob_hex = encrypted_blob_item->valuestring;
|
||||
|
||||
if (!client_pub_hex || !encrypted_blob_hex) {
|
||||
cJSON_Delete(doc);
|
||||
return strdup("{\"error\":\"Missing required fields\"}");
|
||||
}
|
||||
|
||||
printf("\n=== Processing Unlock Request ===\n");
|
||||
printf("Client Public Key: %s\n", client_pub_hex);
|
||||
printf("Encrypted Blob: %s\n", encrypted_blob_hex);
|
||||
|
||||
// Convert client public key from hex
|
||||
size_t client_pub_len = strlen(client_pub_hex) / 2;
|
||||
uint8_t *client_pub_bin = (uint8_t *)malloc(client_pub_len);
|
||||
if (!hex_to_bin(client_pub_hex, client_pub_bin, client_pub_len)) {
|
||||
free(client_pub_bin);
|
||||
cJSON_Delete(doc);
|
||||
return strdup("{\"error\":\"Invalid client public key format\"}");
|
||||
}
|
||||
|
||||
// Parse client public key point
|
||||
mbedtls_ecp_point client_point;
|
||||
mbedtls_ecp_point_init(&client_point);
|
||||
|
||||
int ret = mbedtls_ecp_point_read_binary(&grp, &client_point, client_pub_bin,
|
||||
client_pub_len);
|
||||
free(client_pub_bin);
|
||||
|
||||
if (ret != 0) {
|
||||
mbedtls_ecp_point_free(&client_point);
|
||||
cJSON_Delete(doc);
|
||||
printf("Failed to parse client key: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"Invalid client public key\"}");
|
||||
}
|
||||
|
||||
// Compute shared secret using ECDH
|
||||
mbedtls_mpi shared_secret_mpi;
|
||||
mbedtls_mpi_init(&shared_secret_mpi);
|
||||
|
||||
ret = mbedtls_ecdh_compute_shared(&grp, &shared_secret_mpi, &client_point,
|
||||
&device_private_d,
|
||||
mbedtls_ctr_drbg_random, &ctr_drbg);
|
||||
|
||||
mbedtls_ecp_point_free(&client_point);
|
||||
|
||||
if (ret != 0) {
|
||||
mbedtls_mpi_free(&shared_secret_mpi);
|
||||
cJSON_Delete(doc);
|
||||
printf("ECDH computation failed: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"ECDH failed\"}");
|
||||
}
|
||||
|
||||
// Export shared secret to binary
|
||||
uint8_t shared_secret_raw[32];
|
||||
ret = mbedtls_mpi_write_binary(&shared_secret_mpi, shared_secret_raw, 32);
|
||||
mbedtls_mpi_free(&shared_secret_mpi);
|
||||
|
||||
if (ret != 0) {
|
||||
cJSON_Delete(doc);
|
||||
printf("Shared secret export failed: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"Shared secret export failed\"}");
|
||||
}
|
||||
|
||||
printf("Shared Secret (raw): ");
|
||||
for (int i = 0; i < 32; i++)
|
||||
printf("%02x", shared_secret_raw[i]);
|
||||
printf("\n");
|
||||
|
||||
// Derive separate keys for encryption and authentication
|
||||
// This prevents key reuse vulnerabilities
|
||||
const mbedtls_md_info_t *md_info =
|
||||
mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
|
||||
// Encryption key: SHA256("encryption" || shared_secret)
|
||||
uint8_t enc_key[32];
|
||||
mbedtls_md_context_t md_ctx;
|
||||
mbedtls_md_init(&md_ctx);
|
||||
mbedtls_md_setup(&md_ctx, md_info, 0);
|
||||
mbedtls_md_starts(&md_ctx);
|
||||
mbedtls_md_update(&md_ctx, (const uint8_t *)"encryption", 10);
|
||||
mbedtls_md_update(&md_ctx, shared_secret_raw, 32);
|
||||
mbedtls_md_finish(&md_ctx, enc_key);
|
||||
|
||||
// MAC key: SHA256("authentication" || shared_secret)
|
||||
uint8_t mac_key[32];
|
||||
mbedtls_md_starts(&md_ctx);
|
||||
mbedtls_md_update(&md_ctx, (const uint8_t *)"authentication", 14);
|
||||
mbedtls_md_update(&md_ctx, shared_secret_raw, 32);
|
||||
mbedtls_md_finish(&md_ctx, mac_key);
|
||||
mbedtls_md_free(&md_ctx);
|
||||
|
||||
printf("Encryption Key: ");
|
||||
for (int i = 0; i < 32; i++)
|
||||
printf("%02x", enc_key[i]);
|
||||
printf("\n");
|
||||
|
||||
printf("MAC Key: ");
|
||||
for (int i = 0; i < 32; i++)
|
||||
printf("%02x", mac_key[i]);
|
||||
printf("\n");
|
||||
|
||||
// Securely wipe the raw shared secret
|
||||
memset(shared_secret_raw, 0, 32);
|
||||
|
||||
// Convert encrypted blob from hex
|
||||
// Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80
|
||||
// bytes
|
||||
size_t encrypted_len = strlen(encrypted_blob_hex) / 2;
|
||||
uint8_t *encrypted_blob = (uint8_t *)malloc(encrypted_len);
|
||||
if (!hex_to_bin(encrypted_blob_hex, encrypted_blob, encrypted_len)) {
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
memset(mac_key, 0, 32);
|
||||
cJSON_Delete(doc);
|
||||
return strdup("{\"error\":\"Invalid blob format\"}");
|
||||
}
|
||||
|
||||
// Verify blob length: IV(16) + Ciphertext(32) + HMAC(32) = 80 bytes
|
||||
if (encrypted_len != 80) {
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
memset(mac_key, 0, 32);
|
||||
cJSON_Delete(doc);
|
||||
printf("Expected 80 bytes, got %d\n", encrypted_len);
|
||||
return strdup("{\"error\":\"Invalid blob length\"}");
|
||||
}
|
||||
|
||||
// Extract components from blob
|
||||
uint8_t *iv = encrypted_blob; // First 16 bytes
|
||||
uint8_t *ciphertext = encrypted_blob + 16; // Next 32 bytes
|
||||
uint8_t *received_hmac = encrypted_blob + 48; // Last 32 bytes
|
||||
|
||||
printf("IV: ");
|
||||
for (int i = 0; i < 16; i++)
|
||||
printf("%02x", iv[i]);
|
||||
printf("\n");
|
||||
|
||||
printf("Received HMAC: ");
|
||||
for (int i = 0; i < 32; i++)
|
||||
printf("%02x", received_hmac[i]);
|
||||
printf("\n");
|
||||
|
||||
// ⚠️ CRITICAL: Verify HMAC BEFORE decrypting
|
||||
// This prevents padding oracle attacks and ensures data authenticity
|
||||
uint8_t computed_hmac[32];
|
||||
ret = mbedtls_md_hmac(md_info, mac_key, 32, encrypted_blob, 48,
|
||||
computed_hmac);
|
||||
|
||||
if (ret != 0) {
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
memset(mac_key, 0, 32);
|
||||
cJSON_Delete(doc);
|
||||
printf("HMAC computation failed: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"HMAC computation failed\"}");
|
||||
}
|
||||
|
||||
printf("Computed HMAC: ");
|
||||
for (int i = 0; i < 32; i++)
|
||||
printf("%02x", computed_hmac[i]);
|
||||
printf("\n");
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
int hmac_result = 0;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
hmac_result |= received_hmac[i] ^ computed_hmac[i];
|
||||
}
|
||||
|
||||
// Wipe MAC key after verification
|
||||
memset(mac_key, 0, 32);
|
||||
|
||||
if (hmac_result != 0) {
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
cJSON_Delete(doc);
|
||||
printf("❌ HMAC verification FAILED - ciphertext was modified or wrong "
|
||||
"key!\n");
|
||||
return strdup("{\"error\":\"Authentication failed - data tampered or "
|
||||
"wrong password\"}");
|
||||
}
|
||||
|
||||
printf("✅ HMAC verified - data is authentic\n");
|
||||
|
||||
// Now safe to decrypt (HMAC passed)
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
|
||||
ret = mbedtls_aes_setkey_dec(&aes, enc_key, 256);
|
||||
if (ret != 0) {
|
||||
mbedtls_aes_free(&aes);
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
cJSON_Delete(doc);
|
||||
printf("AES setkey failed: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"AES setup failed\"}");
|
||||
}
|
||||
|
||||
uint8_t *decrypted_data = (uint8_t *)malloc(32);
|
||||
uint8_t iv_copy[16];
|
||||
memcpy(iv_copy, iv, 16); // CBC mode modifies IV
|
||||
|
||||
ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT,
|
||||
32, // data length
|
||||
iv_copy, // IV (will be modified)
|
||||
ciphertext, // input
|
||||
decrypted_data); // output
|
||||
|
||||
mbedtls_aes_free(&aes);
|
||||
free(encrypted_blob);
|
||||
memset(enc_key, 0, 32);
|
||||
|
||||
if (ret != 0) {
|
||||
free(decrypted_data);
|
||||
cJSON_Delete(doc);
|
||||
printf("AES decrypt failed: -0x%04x\n", -ret);
|
||||
return strdup("{\"error\":\"Decryption failed\"}");
|
||||
}
|
||||
|
||||
// Extract the derived key (PBKDF2 hash, 32 bytes)
|
||||
printf("\n=== DECRYPTED DERIVED KEY ===\n");
|
||||
printf("Key (hex): ");
|
||||
for (size_t i = 0; i < 32; i++) {
|
||||
printf("%02x", decrypted_data[i]);
|
||||
}
|
||||
printf("\n");
|
||||
printf("=============================\n\n");
|
||||
|
||||
// Verify the decrypted key against stored password hash
|
||||
bool verification_result = verify_key(decrypted_data, 32);
|
||||
|
||||
cJSON *resp_doc = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(resp_doc, "success", verification_result);
|
||||
|
||||
if (verification_result) {
|
||||
printf("✅ Password verification SUCCESSFUL\n");
|
||||
unlocked = true;
|
||||
cJSON_AddStringToObject(resp_doc, "message", "Unlock successful");
|
||||
} else {
|
||||
printf("❌ Password verification FAILED\n");
|
||||
unlocked = false;
|
||||
cJSON_AddStringToObject(resp_doc, "error", "Invalid password");
|
||||
}
|
||||
|
||||
char *response_str = cJSON_PrintUnformatted(resp_doc);
|
||||
cJSON_Delete(resp_doc);
|
||||
cJSON_Delete(doc);
|
||||
|
||||
// Securely wipe decrypted data
|
||||
memset(decrypted_data, 0, 32);
|
||||
free(decrypted_data);
|
||||
|
||||
*success_out = verification_result;
|
||||
return response_str;
|
||||
}
|
||||
|
||||
// Check if device is unlocked
|
||||
bool is_unlocked() const { return unlocked; }
|
||||
|
||||
// Lock the device
|
||||
void lock() { unlocked = false; }
|
||||
};
|
||||
|
||||
#endif // ZK_AUTH_H
|
||||
159
main/zk_handlers.h
Normal file
159
main/zk_handlers.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#ifndef ZK_HANDLERS_H
|
||||
#define ZK_HANDLERS_H
|
||||
|
||||
#include "provision.h"
|
||||
#include "zk_auth.h"
|
||||
#include "zk_web_page.h"
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG_ZK = "zk_handlers";
|
||||
|
||||
// Global ZK Auth instance (to be initialized in main)
|
||||
extern ZKAuth zk_auth;
|
||||
extern httpd_handle_t server_http;
|
||||
|
||||
// Serve the main web interface
|
||||
static esp_err_t handle_zk_root(httpd_req_t *req) {
|
||||
// Check if provisioning is needed - if so, redirect
|
||||
if (needs_provisioning()) {
|
||||
ESP_LOGI(TAG_ZK, "Provisioning needed - redirecting to /provision");
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", "/provision");
|
||||
httpd_resp_sendstr(req, "Redirecting to provisioning page...");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Normal landing page
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_sendstr(req, ZK_WEB_PAGE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// API endpoint: Get device identity
|
||||
static esp_err_t handle_zk_identity(httpd_req_t *req) {
|
||||
char *json_response = zk_auth.get_identity_json();
|
||||
if (json_response == NULL) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to get identity");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_sendstr(req, json_response);
|
||||
free(json_response);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// API endpoint: Process unlock request
|
||||
static esp_err_t handle_zk_unlock(httpd_req_t *req) {
|
||||
// Read POST body
|
||||
char content[1024];
|
||||
int ret = httpd_req_recv(req, content, sizeof(content) - 1);
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
content[ret] = '\0';
|
||||
|
||||
bool success = false;
|
||||
char *response = zk_auth.process_unlock(content, &success);
|
||||
|
||||
if (response == NULL) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_status(req, success ? "200 OK" : "400 Bad Request");
|
||||
httpd_resp_sendstr(req, response);
|
||||
free(response);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t handle_zk_status(httpd_req_t *req) {
|
||||
unsigned long uptime_ms = esp_timer_get_time() / 1000;
|
||||
char response[128];
|
||||
snprintf(response, sizeof(response), "{\"unlocked\":%s,\"uptime\":%lu}",
|
||||
zk_auth.is_unlocked() ? "true" : "false", uptime_ms);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_sendstr(req, response);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// API endpoint: Lock the device
|
||||
static esp_err_t handle_zk_lock(httpd_req_t *req) {
|
||||
zk_auth.lock();
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_sendstr(req, "{\"unlocked\":false}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Handle CORS preflight
|
||||
static esp_err_t handle_zk_options(httpd_req_t *req) {
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
||||
httpd_resp_set_status(req, "204 No Content");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Register all ZK auth routes to the HTTP server
|
||||
void register_zk_handlers(httpd_handle_t server) {
|
||||
// Root handler for ZK web interface
|
||||
httpd_uri_t root_uri = {.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_zk_root,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &root_uri);
|
||||
|
||||
// API endpoints
|
||||
httpd_uri_t identity_uri = {.uri = "/api/identity",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_zk_identity,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &identity_uri);
|
||||
|
||||
httpd_uri_t status_uri = {.uri = "/api/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = handle_zk_status,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &status_uri);
|
||||
|
||||
httpd_uri_t unlock_uri = {.uri = "/api/unlock",
|
||||
.method = HTTP_POST,
|
||||
.handler = handle_zk_unlock,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &unlock_uri);
|
||||
|
||||
httpd_uri_t unlock_options_uri = {.uri = "/api/unlock",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = handle_zk_options,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &unlock_options_uri);
|
||||
|
||||
httpd_uri_t lock_uri = {.uri = "/api/lock",
|
||||
.method = HTTP_POST,
|
||||
.handler = handle_zk_lock,
|
||||
.user_ctx = NULL};
|
||||
httpd_register_uri_handler(server, &lock_uri);
|
||||
|
||||
ESP_LOGI(TAG_ZK, "ZK Auth routes registered:");
|
||||
ESP_LOGI(TAG_ZK, " GET / - Web interface");
|
||||
ESP_LOGI(TAG_ZK, " GET /api/identity - Device identity");
|
||||
ESP_LOGI(TAG_ZK, " GET /api/status - Session status");
|
||||
ESP_LOGI(TAG_ZK, " POST /api/unlock - Unlock request");
|
||||
ESP_LOGI(TAG_ZK, " POST /api/lock - Lock device");
|
||||
}
|
||||
|
||||
#endif // ZK_HANDLERS_H
|
||||
833
main/zk_web_page.h
Normal file
833
main/zk_web_page.h
Normal file
@@ -0,0 +1,833 @@
|
||||
#ifndef ZK_WEB_PAGE_H
|
||||
#define ZK_WEB_PAGE_H
|
||||
|
||||
// Embedded web interface for Zero-Knowledge Authentication
|
||||
// Includes: HTML, CSS, JavaScript, and crypto libraries (elliptic.js, CryptoJS)
|
||||
|
||||
const char ZK_WEB_PAGE[] = R"rawliteral(
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 Zero-Knowledge Auth</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15'/><path d='M17 2v6'/><path d='M17 4h2'/><path d='M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20'/><circle cx='17' cy='10' r='2'/></svg>">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f0f5f3;
|
||||
border-left: 4px solid #5a7d5a;
|
||||
padding: 15px;
|
||||
margin-bottom: 25px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #4a6d4a;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="password"],
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #5a7d5a;
|
||||
box-shadow: 0 0 0 3px rgba(90, 125, 90, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(90, 125, 90, 0.4);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border: 1px solid #90caf9;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #81c784;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
border: 1px solid #ef5350;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #5a7d5a;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.device-info {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.device-info .label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.device-info .value {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tech-badge {
|
||||
display: inline-block;
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.success-animation {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 30px;
|
||||
border-radius: 50%;
|
||||
background: #e8f5e9;
|
||||
position: relative;
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.checkmark path {
|
||||
stroke: #2e7d32;
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: drawCheck 1s ease-out 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes drawCheck {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-page.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
.unlock-page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unlock-page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e1e8ed;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d1d8dd;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="mainContainer">
|
||||
<div id="unlockPage" class="unlock-page">
|
||||
<h1><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 8px;"><path d="M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15"/><path d="M17 2v6"/><path d="M17 4h2"/><path d="M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><circle cx="17" cy="10" r="2"/></svg>Zero-Knowledge Auth</h1>
|
||||
<p class="subtitle">ESP32-C6 Secure Unlock</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 2px;"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg> Privacy First:</strong> Your password is never transmitted.
|
||||
The device only receives an encrypted, derived key over an ECIES tunnel.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" placeholder="Enter your password" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<button class="btn" onclick="performSecureUnlock()" id="unlockBtn">
|
||||
Unlock Device
|
||||
</button>
|
||||
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div id="statusPage" class="status-page">
|
||||
<div class="success-animation">
|
||||
<div class="checkmark-circle">
|
||||
<svg class="checkmark" viewBox="0 0 52 52">
|
||||
<path d="M14 27l7.5 7.5L38 18"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style="color: #2e7d32; margin-bottom: 10px;">Device Unlocked!</h2>
|
||||
<p style="color: #666;">Authentication successful</p>
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<h3>Session Information</h3>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Status</span>
|
||||
<span class="badge badge-success">Active</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Authentication Method</span>
|
||||
<span class="status-value">Password</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Key Derivation</span>
|
||||
<span class="status-value">PBKDF2-SHA256</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Encryption</span>
|
||||
<span class="status-value">ECIES (P-256 + AES-CBC + HMAC)</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Authenticated At</span>
|
||||
<span class="status-value" id="authTime">--</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Device Uptime</span>
|
||||
<span class="status-value" id="uptime">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="lockDevice()">
|
||||
Lock Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⚠️ SECURITY WARNING: Password handling in browser - client-side only -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/elliptic/6.5.4/elliptic.min.js"></script>
|
||||
<script>
|
||||
// ===============================================================================
|
||||
// Zero-Knowledge Authentication - Browser-Side Cryptography
|
||||
// ===============================================================================
|
||||
// SECURITY FEATURES IMPLEMENTED:
|
||||
// 1. Password is NEVER transmitted - only PBKDF2-derived hash
|
||||
// 2. Immediate password field clearing after read
|
||||
// 3. Secure memory wiping of all sensitive variables (keys, secrets, hashes)
|
||||
// 4. try-finally blocks ensure cleanup even on errors
|
||||
// 5. Ephemeral ECDH keypair (generated per-session, discarded after)
|
||||
// 6. All sensitive data cleared before function returns
|
||||
//
|
||||
// ENCRYPTION: AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC)
|
||||
// - Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80 bytes
|
||||
// - Provides confidentiality (CBC) + authenticity (HMAC)
|
||||
// - Random IV for each session
|
||||
// - HMAC prevents tampering and padding oracle attacks
|
||||
//
|
||||
// PRODUCTION CHECKLIST:
|
||||
// - Remove console.log statements that expose secrets (marked with ⚠️)
|
||||
// - Ensure input field has autocomplete="off" (already set)
|
||||
// - Consider adding Content-Security-Policy headers
|
||||
// - Use subresource integrity (SRI) for CDN libraries in production
|
||||
// ===============================================================================
|
||||
|
||||
let deviceIdentity = null;
|
||||
|
||||
// ⚠️ SECURITY: Secure memory wiping functions
|
||||
function secureWipeArray(arr) {
|
||||
if (!arr) return;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function secureWipeWordArray(wordArray) {
|
||||
if (!wordArray || !wordArray.words) return;
|
||||
for (let i = 0; i < wordArray.words.length; i++) {
|
||||
wordArray.words[i] = 0;
|
||||
}
|
||||
wordArray.sigBytes = 0;
|
||||
}
|
||||
|
||||
function secureWipeString(str) {
|
||||
// Note: JavaScript strings are immutable, but we can at least dereference
|
||||
// Best practice: clear the input field immediately after reading
|
||||
return null;
|
||||
}
|
||||
|
||||
function hexToBytes(hex) {
|
||||
const bytes = [];
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function wordArrayToByteArray(wordArray) {
|
||||
const words = wordArray.words;
|
||||
const sigBytes = wordArray.sigBytes;
|
||||
const bytes = [];
|
||||
for (let i = 0; i < sigBytes; i++) {
|
||||
bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function byteArrayToWordArray(bytes) {
|
||||
const words = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8);
|
||||
}
|
||||
return CryptoJS.lib.WordArray.create(words, bytes.length);
|
||||
}
|
||||
|
||||
async function loadDeviceIdentity() {
|
||||
try {
|
||||
const response = await fetch('/api/identity');
|
||||
if (!response.ok) throw new Error('Failed to fetch device identity');
|
||||
deviceIdentity = await response.json();
|
||||
|
||||
console.log('Device Public Key loaded');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error loading device identity:', error);
|
||||
showStatus('Failed to load device identity', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function performSecureUnlock() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!password) {
|
||||
showStatus('Please enter a password', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('unlockBtn');
|
||||
btn.disabled = true;
|
||||
showStatus('Initializing secure connection...', 'info', true);
|
||||
|
||||
// ⚠️ SECURITY: Track all sensitive variables for cleanup
|
||||
let sessionKeyHash = null;
|
||||
let sessionKeyBytes = null;
|
||||
let sharedSecretBytes = null;
|
||||
let aesKeyHash = null;
|
||||
let aesKeyBytes = null;
|
||||
let clientKey = null;
|
||||
|
||||
try {
|
||||
// Load device identity if not already loaded
|
||||
if (!deviceIdentity) {
|
||||
const loaded = await loadDeviceIdentity();
|
||||
if (!loaded) {
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showStatus('Computing zero-knowledge proof...', 'info', true);
|
||||
|
||||
// Step 1: Derive session key using PBKDF2
|
||||
// PBKDF2-HMAC-SHA256 with 10000 iterations
|
||||
// Use MAC address as salt (received from device identity)
|
||||
const macBytes = hexToBytes(deviceIdentity.macAddress);
|
||||
const salt = byteArrayToWordArray(macBytes);
|
||||
sessionKeyHash = CryptoJS.PBKDF2(password, salt, {
|
||||
keySize: 256/32, // 256 bits = 8 words
|
||||
iterations: 10000,
|
||||
hasher: CryptoJS.algo.SHA256
|
||||
});
|
||||
|
||||
// ⚠️ SECURITY: Clear password from input field immediately
|
||||
passwordInput.value = '';
|
||||
|
||||
sessionKeyBytes = wordArrayToByteArray(sessionKeyHash);
|
||||
const sessionKeyHex = bytesToHex(sessionKeyBytes);
|
||||
|
||||
// ⚠️ PRODUCTION WARNING: Remove console.log statements in production builds
|
||||
// These logs expose sensitive cryptographic material
|
||||
console.log('Salt (MAC Address):', deviceIdentity.macAddress);
|
||||
console.log('Session Key (PBKDF2):', sessionKeyHex);
|
||||
|
||||
showStatus('Establishing ECIES tunnel...', 'info', true);
|
||||
|
||||
// Step 2: Generate ephemeral client keypair using elliptic
|
||||
const ec = new elliptic.ec('p256');
|
||||
clientKey = ec.genKeyPair();
|
||||
|
||||
// Export uncompressed public key (0x04 + X + Y)
|
||||
const clientPubHex = clientKey.getPublic('hex');
|
||||
console.log('Client Public Key:', clientPubHex); // Public key - safe to log
|
||||
|
||||
// Step 3: Import server public key and derive shared secret (ECDH)
|
||||
const serverKey = ec.keyFromPublic(deviceIdentity.pubKey, 'hex');
|
||||
const sharedPoint = clientKey.derive(serverKey.getPublic());
|
||||
|
||||
// Convert BN to 32-byte array
|
||||
const sharedSecretHex = sharedPoint.toString(16).padStart(64, '0');
|
||||
sharedSecretBytes = hexToBytes(sharedSecretHex);
|
||||
|
||||
// ⚠️ PRODUCTION WARNING: Remove in production - exposes shared secret
|
||||
console.log('Shared Secret:', sharedSecretHex);
|
||||
|
||||
// Step 4: Derive separate keys for encryption and authentication
|
||||
// This prevents key reuse vulnerabilities in Encrypt-then-MAC
|
||||
const sharedSecretWA = byteArrayToWordArray(sharedSecretBytes);
|
||||
|
||||
// Encryption key: SHA256("encryption" || shared_secret)
|
||||
const encKeyHash = CryptoJS.SHA256(
|
||||
CryptoJS.enc.Utf8.parse('encryption').concat(sharedSecretWA)
|
||||
);
|
||||
const encKeyBytes = wordArrayToByteArray(encKeyHash);
|
||||
|
||||
// MAC key: SHA256("authentication" || shared_secret)
|
||||
const macKeyHash = CryptoJS.SHA256(
|
||||
CryptoJS.enc.Utf8.parse('authentication').concat(sharedSecretWA)
|
||||
);
|
||||
const macKeyBytes = wordArrayToByteArray(macKeyHash);
|
||||
|
||||
// ⚠️ PRODUCTION WARNING: Remove in production - exposes keys
|
||||
console.log('Encryption Key:', bytesToHex(encKeyBytes));
|
||||
console.log('MAC Key:', bytesToHex(macKeyBytes));
|
||||
|
||||
// ⚠️ SECURITY: Clear shared secret after deriving keys
|
||||
secureWipeArray(sharedSecretBytes);
|
||||
secureWipeWordArray(sharedSecretWA);
|
||||
|
||||
showStatus('Encrypting credentials...', 'info', true);
|
||||
|
||||
// Step 5: Encrypt the session key hash with AES-256-CBC
|
||||
|
||||
// Generate random IV (16 bytes for CBC)
|
||||
const ivWords = CryptoJS.lib.WordArray.random(16);
|
||||
|
||||
// Convert encryption key bytes to WordArray
|
||||
const encKey = byteArrayToWordArray(encKeyBytes);
|
||||
|
||||
// Encrypt with AES-256-CBC using NoPadding
|
||||
// Session key hash is 32 bytes (exactly 2 blocks), so no padding needed
|
||||
const encrypted = CryptoJS.AES.encrypt(
|
||||
sessionKeyHash,
|
||||
encKey,
|
||||
{
|
||||
iv: ivWords,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.NoPadding
|
||||
}
|
||||
);
|
||||
|
||||
// ⚠️ SECURITY: Clear session key after encryption
|
||||
secureWipeArray(sessionKeyBytes);
|
||||
secureWipeWordArray(sessionKeyHash);
|
||||
|
||||
// Extract IV and ciphertext
|
||||
const ivBytes = wordArrayToByteArray(ivWords);
|
||||
const ciphertextBytes = wordArrayToByteArray(encrypted.ciphertext);
|
||||
|
||||
showStatus('Computing authentication tag...', 'info', true);
|
||||
|
||||
// Build complete blob: IV (16) + Ciphertext (32) + HMAC (32) = 80 bytes
|
||||
// Note: We only include first 32 bytes of ciphertext (should be exactly 32 after padding)
|
||||
const completeBlob = new Uint8Array(80);
|
||||
completeBlob.set(ivBytes, 0); // IV at offset 0
|
||||
completeBlob.set(ciphertextBytes.slice(0, 32), 16); // First 32 bytes of ciphertext at offset 16
|
||||
|
||||
// Step 6: Compute HMAC-SHA256 over IV + Ciphertext (Encrypt-then-MAC)
|
||||
// CRITICAL: Compute HMAC over the EXACT data in the blob (first 48 bytes: IV + CT)
|
||||
const dataToAuthenticateBytes = new Uint8Array(48);
|
||||
dataToAuthenticateBytes.set(completeBlob.slice(0, 48));
|
||||
const dataToAuthenticate = byteArrayToWordArray(Array.from(dataToAuthenticateBytes));
|
||||
const macKey = byteArrayToWordArray(macKeyBytes);
|
||||
const hmac = CryptoJS.HmacSHA256(dataToAuthenticate, macKey);
|
||||
const hmacBytes = wordArrayToByteArray(hmac);
|
||||
|
||||
console.log('IV:', bytesToHex(ivBytes));
|
||||
console.log('HMAC:', bytesToHex(hmacBytes));
|
||||
|
||||
// Place HMAC in blob
|
||||
completeBlob.set(hmacBytes, 48); // HMAC at offset 48
|
||||
|
||||
const encryptedBlobHex = bytesToHex(Array.from(completeBlob));
|
||||
|
||||
console.log('Encrypted Blob (IV+CT+HMAC):', encryptedBlobHex); // Encrypted data - safe to log
|
||||
|
||||
// ⚠️ SECURITY: Clear all keys after encryption
|
||||
secureWipeArray(encKeyBytes);
|
||||
secureWipeArray(macKeyBytes);
|
||||
secureWipeWordArray(encKeyHash);
|
||||
secureWipeWordArray(macKeyHash);
|
||||
secureWipeWordArray(encKey);
|
||||
secureWipeWordArray(macKey);
|
||||
secureWipeWordArray(ivWords);
|
||||
secureWipeWordArray(dataToAuthenticate);
|
||||
secureWipeWordArray(hmac);
|
||||
|
||||
showStatus('Sending unlock request...', 'info', true);
|
||||
|
||||
// Step 7: Send to device
|
||||
const response = await fetch('/api/unlock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientPub: clientPubHex,
|
||||
blob: encryptedBlobHex
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Unlock request failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showStatus('✅ Device unlocked successfully!', 'success');
|
||||
|
||||
// Reload page after 1.5s - server will show status page
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showStatus('❌ Unlock failed: ' + (result.error || 'Unknown error'), 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showStatus('❌ Error: ' + error.message, 'error');
|
||||
} finally {
|
||||
// ⚠️ SECURITY: Always clear sensitive data, even on error
|
||||
btn.disabled = false;
|
||||
passwordInput.value = ''; // Ensure password field is cleared
|
||||
|
||||
// Wipe all sensitive variables
|
||||
if (sessionKeyHash) secureWipeWordArray(sessionKeyHash);
|
||||
if (sessionKeyBytes) secureWipeArray(sessionKeyBytes);
|
||||
if (sharedSecretBytes) secureWipeArray(sharedSecretBytes);
|
||||
|
||||
// Clear client private key if possible
|
||||
if (clientKey && clientKey.priv) {
|
||||
// Elliptic.js uses BN.js for private keys - zero it out
|
||||
if (clientKey.priv.words) {
|
||||
for (let i = 0; i < clientKey.priv.words.length; i++) {
|
||||
clientKey.priv.words[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type, showLoader = false) {
|
||||
const status = document.getElementById('status');
|
||||
status.className = 'status-' + type;
|
||||
status.style.display = 'block';
|
||||
|
||||
if (showLoader) {
|
||||
status.innerHTML = '<div class="loader"></div>' + message;
|
||||
} else {
|
||||
status.innerHTML = message;
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function lockDevice() {
|
||||
await fetch('/api/lock', { method: 'POST' });
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function formatUptime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
// On load: ask server which view to show
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
const res = await fetch('/api/status');
|
||||
const data = await res.json();
|
||||
if (data.unlocked) {
|
||||
document.getElementById('authTime').textContent = new Date().toLocaleString();
|
||||
if (data.uptime !== undefined) {
|
||||
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
||||
}
|
||||
document.getElementById('statusPage').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('unlockPage').classList.add('active');
|
||||
loadDeviceIdentity();
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('unlockPage').classList.add('active');
|
||||
loadDeviceIdentity();
|
||||
}
|
||||
});
|
||||
|
||||
// Allow Enter key to submit
|
||||
document.getElementById('password').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
performSecureUnlock();
|
||||
}
|
||||
});
|
||||
|
||||
// ⚠️ SECURITY: Clear password field on page unload/navigation
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const passwordInput = document.getElementById('password');
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// ⚠️ SECURITY: Clear password field when page is restored from bfcache
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted) {
|
||||
// Page was restored from back/forward cache
|
||||
const passwordInput = document.getElementById('password');
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)rawliteral";
|
||||
|
||||
#endif // ZK_WEB_PAGE_H
|
||||
Reference in New Issue
Block a user