From aba48e463ff0b96bf06552f5a0471895aa247b74 Mon Sep 17 00:00:00 2001 From: Jonathan Berrisch Date: Sun, 1 Mar 2026 20:12:02 +0100 Subject: [PATCH] Restructure and improve --- main/CMakeLists.txt | 7 +- main/Kconfig.projbuild | 32 +++- main/TangServer.h | 169 ----------------- main/atecc608a.c | 103 ++++++++++ main/atecc608a.h | 414 +---------------------------------------- main/main.c | 281 ++++++++++++++++++++++++++++ main/main.cpp | 12 -- main/ssh_client.c | 327 ++++++++++++++++++++++++++++++++ main/ssh_client.h | 25 +++ main/wifi.c | 81 ++++++++ main/wifi.h | 19 ++ main/zk_auth.h | 411 ---------------------------------------- main/zk_handlers.h | 50 ----- sdkconfig.defaults | 6 +- 14 files changed, 887 insertions(+), 1050 deletions(-) delete mode 100644 main/TangServer.h create mode 100644 main/atecc608a.c create mode 100644 main/main.c delete mode 100644 main/main.cpp create mode 100644 main/ssh_client.c create mode 100644 main/ssh_client.h create mode 100644 main/wifi.c create mode 100644 main/wifi.h delete mode 100644 main/zk_auth.h delete mode 100644 main/zk_handlers.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f2cbcc8..1c39a2b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,6 @@ -idf_component_register(SRCS "main.cpp" +idf_component_register(SRCS "main.c" + "wifi.c" + "atecc608a.c" + "ssh_client.c" INCLUDE_DIRS "." - REQUIRES mbedtls esp-cryptoauthlib esp_http_server esp_wifi nvs_flash json esp_timer efuse) + REQUIRES mbedtls esp-cryptoauthlib esp_wifi nvs_flash driver) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index ffd57da..05413b4 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,4 +1,4 @@ -menu "Wi-Fi Credentials" +menu "KeyPi" config WIFI_SSID string "Wi-Fi SSID" @@ -12,4 +12,34 @@ config WIFI_PASSWORD help Password for the Wi-Fi network. +config SSH_HOSTNAME + string "SSH Server Hostname or IP" + default "192.168.1.1" + help + Hostname or IP address of the SSH server (door controller). + +config SSH_PORT + int "SSH Server Port" + default 22 + help + TCP port of the SSH server. + +config SSH_USERNAME + string "SSH Username" + default "pi" + help + Username to authenticate with on the SSH server. + +config SSH_OPEN_COMMAND + string "SSH Open Command" + default "door open" + help + Shell command to execute on the SSH server to open the door. + +config SSH_LOCK_COMMAND + string "SSH Lock Command" + default "door lock" + help + Shell command to execute on the SSH server to lock the door. + endmenu diff --git a/main/TangServer.h b/main/TangServer.h deleted file mode 100644 index 4b5660e..0000000 --- a/main/TangServer.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef TANG_SERVER_H -#define TANG_SERVER_H - -#include "sdkconfig.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static const char *TAG = "TangServer"; - -// Include core components -#include "atecc608a.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; -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() { - - 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_zk_handlers(server); - ESP_LOGI(TAG, "HTTP server listening on port 80"); - } else { - ESP_LOGE(TAG, "Failed to start HTTP server"); - } - - return server; -} - -// --- Main Setup --- -void setup() { - - // 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"); - - if (atecc608B_init()) { - atecc608B_print_config(); - } else { - ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed"); - } - - // 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:///"); - ESP_LOGI(TAG, " - Tang Server: http:///adv"); - } -} - -// --- Main Loop --- -void loop() { - // Just delay - HTTP server handles requests in its own task - vTaskDelay(pdMS_TO_TICKS(1000)); -} - -#endif // TANG_SERVER_H diff --git a/main/atecc608a.c b/main/atecc608a.c new file mode 100644 index 0000000..90ce8d1 --- /dev/null +++ b/main/atecc608a.c @@ -0,0 +1,103 @@ +#include "atecc608a.h" + +#include "sdkconfig.h" +#include +#include +#include +#include +#include +#include + +static const char *TAG = "atecc608b"; + +/* ------------------------------------------------------------------------- + * Module-private state + * ---------------------------------------------------------------------- */ +static ATCAIfaceCfg s_atecc_cfg; +static uint8_t s_config_data[128]; +static bool s_config_valid = false; + +/* ------------------------------------------------------------------------- + * Internal helpers + * ---------------------------------------------------------------------- */ +static bool read_config_zone(void) +{ + ATCA_STATUS status = atcab_read_config_zone(s_config_data); + + if (status != ATCA_SUCCESS) { + ESP_LOGI(TAG, "Failed to read config zone (0x%02X) – check wiring", status); + return false; + } + + s_config_valid = true; + return true; +} + +/* ------------------------------------------------------------------------- + * Public API + * ---------------------------------------------------------------------- */ +bool atecc608B_init(void) +{ + ESP_LOGI(TAG, "Initialising ATECC608B..."); + + s_atecc_cfg.iface_type = ATCA_I2C_IFACE; + s_atecc_cfg.devtype = ATECC608B; + s_atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS; + s_atecc_cfg.atcai2c.bus = 0; + s_atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE; + s_atecc_cfg.wake_delay = 1500; + s_atecc_cfg.rx_retries = 20; + + ESP_LOGI(TAG, "SDA=GPIO%d SCL=GPIO%d addr=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); + + ATCA_STATUS status = atcab_init(&s_atecc_cfg); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG, "atcab_init failed: 0x%02X", status); + return false; + } + + vTaskDelay(pdMS_TO_TICKS(100)); + + status = atcab_wakeup(); + if (status != ATCA_SUCCESS) { + ESP_LOGW(TAG, "Wake returned 0x%02X (may be normal)", status); + } + atcab_idle(); + + vTaskDelay(pdMS_TO_TICKS(50)); + + ESP_LOGI(TAG, "ATECC608B initialised"); + return true; +} + +void atecc608B_print_config(void) +{ + ESP_LOGI(TAG, "=== ATECC608B Configuration Zone ==="); + + uint8_t serial[9]; + ATCA_STATUS status = atcab_read_serial_number(serial); + if (status != ATCA_SUCCESS) { + ESP_LOGE(TAG, "Failed to read serial number (0x%02X) – check wiring", status); + return; + } + + if (!read_config_zone()) { + return; + } + uint8_t *c = s_config_data; + + for (int i = 0; i < 128; i++) { + if (i % 16 == 0) printf("\n0x%02X: ", i); + printf("%02X ", c[i]); + } + printf("\n\n"); + + ESP_LOGI(TAG, "=== End of ATECC608B Configuration ==="); +} + +void atecc608B_release(void) +{ + atcab_release(); +} diff --git a/main/atecc608a.h b/main/atecc608a.h index e4ff74f..ca3dc38 100644 --- a/main/atecc608a.h +++ b/main/atecc608a.h @@ -2,417 +2,23 @@ #define ATECC608B_H #include "cryptoauthlib.h" -#include "sdkconfig.h" -#include -#include -#include -#include - -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; +#include /** - * Read ATECC608B configuration zone into global buffer + * Initialise the ATECC608B over I2C. + * Returns true on success. */ -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; -} +bool atecc608B_init(void); /** - * Print ATECC608B configuration zone to console with detailed subzone breakdown - * According to ATECC608B Table 2-4 + * Read the full configuration zone and print it to the console. + * Useful for first-time setup diagnostics. */ -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"); -} +void atecc608B_print_config(void); /** - * Get ATECC608B configuration as JSON string (caller must free the returned - * string) + * Release cryptoauthlib resources. */ -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; - } +void atecc608B_release(void); - 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 +#endif /* ATECC608B_H */ diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..96e6a45 --- /dev/null +++ b/main/main.c @@ -0,0 +1,281 @@ +/* + * keypitecc – door controller + * + * Hardware: + * GPIO 9 – Boot button (active-low, press = GND) + * GPIO 15 – User LED (active-high, 1 = on) + * + * Button state machine (5-second window): + * IDLE + * └─ press ──► PENDING_OPEN (slow blink, will run SSH open command) + * └─ press ──► PENDING_LOCK (fast blink, will run SSH lock command) + * └─ press ──► IDLE (cancelled) + * Any pending state expires after 5 s → command is executed → IDLE + * + * LED idle behaviour: + * WiFi connected → LED ON + * WiFi disconnected → LED OFF + */ + +#include "atecc608a.h" +#include "ssh_client.h" +#include "wifi.h" + +#include "sdkconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "main"; + +/* ------------------------------------------------------------------------- + * GPIO defines + * ---------------------------------------------------------------------- */ +#define GPIO_BTN 9 /* Boot button, active-low */ +#define GPIO_LED 15 /* User LED, active-high */ + +/* ------------------------------------------------------------------------- + * Button / LED types + * ---------------------------------------------------------------------- */ +typedef enum { + BTN_STATE_IDLE, + BTN_STATE_PENDING_OPEN, + BTN_STATE_PENDING_LOCK, +} btn_state_t; + +typedef enum { + LED_MODE_WIFI_STATUS, /* ON = connected, OFF = disconnected */ + LED_MODE_SLOW_BLINK, /* 500 ms on / 500 ms off (open pending) */ + LED_MODE_FAST_BLINK, /* 100 ms on / 100 ms off (lock pending) */ +} led_mode_t; + +typedef enum { + EVT_BUTTON_PRESS, + EVT_TIMER_EXPIRED, +} event_type_t; + +/* ------------------------------------------------------------------------- + * Shared state + * ---------------------------------------------------------------------- */ +static volatile led_mode_t s_led_mode = LED_MODE_WIFI_STATUS; +static volatile btn_state_t s_btn_state = BTN_STATE_IDLE; + +static QueueHandle_t s_event_queue; +static TimerHandle_t s_action_timer; + +/* Debounce: ignore button edges within 200 ms of the previous one */ +static volatile TickType_t s_last_btn_tick = 0; + +/* ------------------------------------------------------------------------- + * SSH task – spawned on demand so the button task stays responsive + * ---------------------------------------------------------------------- */ +static void ssh_task(void *arg) +{ + const char *cmd = (const char *)arg; + ESP_LOGI(TAG, "Executing SSH command: %s", cmd); + bool ok = ssh_execute_command(cmd); + ESP_LOGI(TAG, "SSH command %s: %s", cmd, ok ? "succeeded" : "FAILED"); + vTaskDelete(NULL); +} + +static void trigger_ssh(const char *cmd) +{ + /* Stack size 16 kB – SSH needs heap + TLS, keep priority low */ + if (xTaskCreate(ssh_task, "ssh", 16384, (void *)cmd, 3, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create SSH task"); + } +} + +/* ------------------------------------------------------------------------- + * FreeRTOS software timer callback – runs in timer daemon task context + * ---------------------------------------------------------------------- */ +static void action_timer_cb(TimerHandle_t xTimer) +{ + (void)xTimer; + event_type_t evt = EVT_TIMER_EXPIRED; + xQueueSend(s_event_queue, &evt, 0); +} + +/* ------------------------------------------------------------------------- + * GPIO ISR + * ---------------------------------------------------------------------- */ +static void IRAM_ATTR button_isr(void *arg) +{ + (void)arg; + TickType_t now = xTaskGetTickCountFromISR(); + if ((now - s_last_btn_tick) < pdMS_TO_TICKS(200)) { + return; /* debounce */ + } + s_last_btn_tick = now; + + event_type_t evt = EVT_BUTTON_PRESS; + xQueueSendFromISR(s_event_queue, &evt, NULL); +} + +/* ------------------------------------------------------------------------- + * Button task – owns the state machine + * ---------------------------------------------------------------------- */ +static void button_task(void *arg) +{ + (void)arg; + event_type_t evt; + + for (;;) { + if (xQueueReceive(s_event_queue, &evt, portMAX_DELAY) != pdTRUE) { + continue; + } + + switch (s_btn_state) { + + case BTN_STATE_IDLE: + if (evt == EVT_BUTTON_PRESS) { + ESP_LOGI(TAG, "Button: PENDING_OPEN in 5s (press again for LOCK, third press cancels)"); + s_btn_state = BTN_STATE_PENDING_OPEN; + s_led_mode = LED_MODE_SLOW_BLINK; + xTimerStart(s_action_timer, 0); + } + break; + + case BTN_STATE_PENDING_OPEN: + if (evt == EVT_BUTTON_PRESS) { + ESP_LOGI(TAG, "Button: switching to PENDING_LOCK"); + s_btn_state = BTN_STATE_PENDING_LOCK; + s_led_mode = LED_MODE_FAST_BLINK; + xTimerReset(s_action_timer, 0); /* restart 5s from now */ + } else if (evt == EVT_TIMER_EXPIRED) { + ESP_LOGI(TAG, "Timer expired: executing OPEN command"); + s_btn_state = BTN_STATE_IDLE; + s_led_mode = LED_MODE_WIFI_STATUS; + trigger_ssh(CONFIG_SSH_OPEN_COMMAND); + } + break; + + case BTN_STATE_PENDING_LOCK: + if (evt == EVT_BUTTON_PRESS) { + ESP_LOGI(TAG, "Button: action CANCELLED"); + xTimerStop(s_action_timer, 0); + s_btn_state = BTN_STATE_IDLE; + s_led_mode = LED_MODE_WIFI_STATUS; + } else if (evt == EVT_TIMER_EXPIRED) { + ESP_LOGI(TAG, "Timer expired: executing LOCK command"); + s_btn_state = BTN_STATE_IDLE; + s_led_mode = LED_MODE_WIFI_STATUS; + trigger_ssh(CONFIG_SSH_LOCK_COMMAND); + } + break; + } + } +} + +/* ------------------------------------------------------------------------- + * LED task – drives GPIO 15 according to the current led_mode + * ---------------------------------------------------------------------- */ +static void led_task(void *arg) +{ + (void)arg; + bool led_state = false; + + for (;;) { + led_mode_t mode = s_led_mode; /* atomic read (single-byte enum) */ + + switch (mode) { + case LED_MODE_WIFI_STATUS: + led_state = wifi_is_connected(); + gpio_set_level(GPIO_LED, led_state ? 0 : 1); + vTaskDelay(pdMS_TO_TICKS(250)); + break; + + case LED_MODE_SLOW_BLINK: + gpio_set_level(GPIO_LED, 1); + vTaskDelay(pdMS_TO_TICKS(500)); + gpio_set_level(GPIO_LED, 0); + vTaskDelay(pdMS_TO_TICKS(500)); + break; + + case LED_MODE_FAST_BLINK: + gpio_set_level(GPIO_LED, 1); + vTaskDelay(pdMS_TO_TICKS(100)); + gpio_set_level(GPIO_LED, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + break; + } + } +} + +/* ------------------------------------------------------------------------- + * GPIO initialisation + * ---------------------------------------------------------------------- */ +static void gpio_init(void) +{ + /* LED output */ + gpio_config_t led_cfg = { + .pin_bit_mask = (1ULL << GPIO_LED), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&led_cfg); + gpio_set_level(GPIO_LED, 1); /* off (active-high) */ + + /* Button input with interrupt on falling edge (active-low) */ + gpio_config_t btn_cfg = { + .pin_bit_mask = (1ULL << GPIO_BTN), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_NEGEDGE, + }; + gpio_config(&btn_cfg); + + gpio_install_isr_service(0); + gpio_isr_handler_add(GPIO_BTN, button_isr, NULL); +} + +/* ------------------------------------------------------------------------- + * app_main + * ---------------------------------------------------------------------- */ +void app_main(void) +{ + /* NVS (required by WiFi) */ + 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); + + /* ATECC608B */ + if (!atecc608B_init()) { + ESP_LOGW(TAG, "ATECC608B init failed – SSH authentication will not work"); + } else { + atecc608B_print_config(); + ssh_print_public_key(); /* print key for authorized_keys setup */ + } + + /* WiFi */ + wifi_init(); + + /* GPIO and event infrastructure */ + gpio_init(); + + s_event_queue = xQueueCreate(8, sizeof(event_type_t)); + configASSERT(s_event_queue); + + /* 5-second one-shot timer (auto-reload disabled) */ + s_action_timer = xTimerCreate("action", pdMS_TO_TICKS(5000), + pdFALSE, NULL, action_timer_cb); + configASSERT(s_action_timer); + + /* Tasks */ + xTaskCreate(button_task, "button", 4096, NULL, 5, NULL); + xTaskCreate(led_task, "led", 2048, NULL, 4, NULL); + + ESP_LOGI(TAG, "keypitecc ready – press the boot button to operate the door"); +} diff --git a/main/main.cpp b/main/main.cpp deleted file mode 100644 index c5832d2..0000000 --- a/main/main.cpp +++ /dev/null @@ -1,12 +0,0 @@ -/* - * ESP32 Tang Server - Main Entry Point - */ -#include "TangServer.h" - -extern "C" void app_main(void) { - setup(); - - while (true) { - loop(); - } -} diff --git a/main/ssh_client.c b/main/ssh_client.c new file mode 100644 index 0000000..a001028 --- /dev/null +++ b/main/ssh_client.c @@ -0,0 +1,327 @@ +#include "ssh_client.h" + +#include "cryptoauthlib.h" +#include "sdkconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "ssh_client"; + +/* ------------------------------------------------------------------------- + * SSH blob / key helpers + * ---------------------------------------------------------------------- */ + +/* Write a big-endian uint32 into buf. */ +static void write_be32(uint8_t *buf, uint32_t val) +{ + buf[0] = (val >> 24) & 0xFF; + buf[1] = (val >> 16) & 0xFF; + buf[2] = (val >> 8) & 0xFF; + buf[3] = val & 0xFF; +} + +/** + * Format one ECDSA integer as an SSH mpint: + * 4-byte big-endian length | optional 0x00 pad | value (leading zeros stripped) + * + * Returns number of bytes written to buf. + */ +static uint32_t write_mpint(uint8_t *buf, const uint8_t *val, uint32_t size) +{ + uint32_t start = 0; + while (start < (size - 1) && val[start] == 0x00) { + start++; + } + + bool pad = (val[start] & 0x80) != 0; + uint32_t len = (size - start) + (pad ? 1 : 0); + + uint32_t off = 0; + buf[off++] = (len >> 24) & 0xFF; + buf[off++] = (len >> 16) & 0xFF; + buf[off++] = (len >> 8) & 0xFF; + buf[off++] = len & 0xFF; + + if (pad) { + buf[off++] = 0x00; + } + memcpy(&buf[off], &val[start], size - start); + return off + (size - start); +} + +/** + * Build a 104-byte SSH public key blob for ecdsa-sha2-nistp256: + * [uint32 19] "ecdsa-sha2-nistp256" + * [uint32 8] "nistp256" + * [uint32 65] 0x04 || X(32) || Y(32) + * + * @param atecc_pubkey 64-byte raw public key (X||Y) from ATECC608B. + * @param out_blob Must point to a buffer of at least 104 bytes. + */ +static void build_pubkey_blob(const uint8_t *atecc_pubkey, uint8_t *out_blob) +{ + uint32_t off = 0; + + /* Key type */ + const char *key_type = "ecdsa-sha2-nistp256"; + write_be32(&out_blob[off], 19); off += 4; + memcpy(&out_blob[off], key_type, 19); off += 19; + + /* Curve name */ + const char *curve = "nistp256"; + write_be32(&out_blob[off], 8); off += 4; + memcpy(&out_blob[off], curve, 8); off += 8; + + /* Uncompressed EC point: 0x04 || X || Y */ + write_be32(&out_blob[off], 65); off += 4; + out_blob[off++] = 0x04; + memcpy(&out_blob[off], atecc_pubkey, 64); + /* off += 64 — total = 104 */ +} + +/* ------------------------------------------------------------------------- + * libssh2 signing callback (ATECC608B signs the hash) + * ---------------------------------------------------------------------- */ +static int sign_callback(LIBSSH2_SESSION *session, + unsigned char **sig, size_t *sig_len, + const unsigned char *data, size_t data_len, + void **abstract) +{ + (void)session; + (void)abstract; + + uint8_t digest[32]; + uint8_t raw_sig[64]; + + /* Hash the challenge data */ + mbedtls_sha256(data, data_len, digest, 0); + + /* Sign with the ATECC608B hardware key in slot 0 */ + if (atcab_sign(0, digest, raw_sig) != ATCA_SUCCESS) { + ESP_LOGE(TAG, "ATECC608B signing failed!"); + return -1; + } + + /* Encode as [mpint R][mpint S] (no outer string wrapper) */ + unsigned char *buf = (unsigned char *)malloc(80); + if (!buf) return -1; + + uint32_t off = 0; + off += write_mpint(&buf[off], &raw_sig[0], 32); /* R */ + off += write_mpint(&buf[off], &raw_sig[32], 32); /* S */ + + *sig = buf; + *sig_len = off; + return 0; +} + +/* ------------------------------------------------------------------------- + * Helpers for non-blocking libssh2 + * ---------------------------------------------------------------------- */ +static int waitsocket(int sock_fd, LIBSSH2_SESSION *session) +{ + struct timeval tv = { .tv_sec = 10, .tv_usec = 0 }; + fd_set fd; + FD_ZERO(&fd); + FD_SET(sock_fd, &fd); + + fd_set *rfd = NULL, *wfd = NULL; + int dir = libssh2_session_block_directions(session); + if (dir & LIBSSH2_SESSION_BLOCK_INBOUND) rfd = &fd; + if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) wfd = &fd; + + return select(sock_fd + 1, rfd, wfd, NULL, &tv); +} + +/* ------------------------------------------------------------------------- + * Public API + * ---------------------------------------------------------------------- */ +void ssh_print_public_key(void) +{ + uint8_t raw_key[64]; + ATCA_STATUS st = atcab_get_pubkey(0, raw_key); + if (st != ATCA_SUCCESS) { + ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st); + return; + } + + uint8_t blob[104]; + build_pubkey_blob(raw_key, blob); + + size_t b64_len = 0; + mbedtls_base64_encode(NULL, 0, &b64_len, blob, sizeof(blob)); + + unsigned char *b64 = (unsigned char *)malloc(b64_len + 1); + if (!b64) { + ESP_LOGE(TAG, "malloc failed for base64 buffer"); + return; + } + mbedtls_base64_encode(b64, b64_len, &b64_len, blob, sizeof(blob)); + b64[b64_len] = '\0'; + + printf("\n=== SSH Public Key (add to authorized_keys) ===\n"); + printf("ecdsa-sha2-nistp256 %s keypitecc\n", b64); + printf("================================================\n\n"); + + free(b64); +} + +bool ssh_execute_command(const char *cmd) +{ + if (!cmd) return false; + + /* --- Read public key blob ------------------------------------------ */ + uint8_t raw_key[64]; + ATCA_STATUS st = atcab_get_pubkey(0, raw_key); + if (st != ATCA_SUCCESS) { + ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st); + return false; + } + uint8_t pubkey_blob[104]; + build_pubkey_blob(raw_key, pubkey_blob); + + /* --- TCP connect ------------------------------------------------------- */ + int rc; + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + ESP_LOGE(TAG, "socket() failed"); + return false; + } + + struct sockaddr_in sin = {}; + sin.sin_family = AF_INET; + sin.sin_port = htons(CONFIG_SSH_PORT); + sin.sin_addr.s_addr = inet_addr(CONFIG_SSH_HOSTNAME); + + if (sin.sin_addr.s_addr == 0xFFFFFFFF) { + struct hostent *hp = gethostbyname(CONFIG_SSH_HOSTNAME); + if (!hp) { + ESP_LOGE(TAG, "gethostbyname(%s) failed", CONFIG_SSH_HOSTNAME); + close(sock); + return false; + } + sin.sin_addr.s_addr = ((struct ip4_addr *)hp->h_addr)->addr; + } + + if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) != 0) { + ESP_LOGE(TAG, "connect to %s:%d failed", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT); + close(sock); + return false; + } + ESP_LOGI(TAG, "TCP connected to %s:%d", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT); + + /* --- libssh2 session -------------------------------------------------- */ + rc = libssh2_init(0); + if (rc) { + ESP_LOGE(TAG, "libssh2_init failed: %d", rc); + close(sock); + return false; + } + + LIBSSH2_SESSION *session = libssh2_session_init(); + if (!session) { + ESP_LOGE(TAG, "libssh2_session_init failed"); + libssh2_exit(); + close(sock); + return false; + } + + rc = libssh2_session_handshake(session, sock); + if (rc) { + ESP_LOGE(TAG, "SSH handshake failed: %d", rc); + goto cleanup_session; + } + ESP_LOGI(TAG, "SSH handshake OK"); + + /* --- Authenticate with ATECC608B hardware key ------------------------- */ + void *abstract = NULL; + rc = libssh2_userauth_publickey(session, CONFIG_SSH_USERNAME, + pubkey_blob, sizeof(pubkey_blob), + sign_callback, &abstract); + if (rc != 0) { + ESP_LOGE(TAG, "SSH public-key authentication failed: %d", rc); + goto cleanup_session; + } + ESP_LOGI(TAG, "SSH authenticated as '%s'", CONFIG_SSH_USERNAME); + + /* --- Open channel and execute command --------------------------------- */ + LIBSSH2_CHANNEL *channel; + do { + channel = libssh2_channel_open_session(session); + if (!channel && libssh2_session_last_errno(session) == LIBSSH2_ERROR_EAGAIN) { + waitsocket(sock, session); + } + } while (!channel); + + if (!channel) { + ESP_LOGE(TAG, "Failed to open SSH channel"); + goto cleanup_session; + } + + ESP_LOGI(TAG, "Executing: %s", cmd); + while ((rc = libssh2_channel_exec(channel, cmd)) == LIBSSH2_ERROR_EAGAIN) { + waitsocket(sock, session); + } + if (rc != 0) { + ESP_LOGE(TAG, "channel_exec failed: %d", rc); + libssh2_channel_free(channel); + goto cleanup_session; + } + + /* --- Read output ------------------------------------------------------- */ + char buf[256]; + int bytes; + ESP_LOGI(TAG, "--- command output ---"); + for (;;) { + do { + bytes = libssh2_channel_read(channel, buf, sizeof(buf) - 1); + } while (bytes == LIBSSH2_ERROR_EAGAIN && (waitsocket(sock, session), 1)); + + if (bytes <= 0) break; + buf[bytes] = '\0'; + printf("%s", buf); + } + ESP_LOGI(TAG, "--- end output ---"); + + /* --- Shutdown --------------------------------------------------------- */ + while ((rc = libssh2_channel_send_eof(channel)) == LIBSSH2_ERROR_EAGAIN) { + waitsocket(sock, session); + } + while (libssh2_channel_wait_eof(channel) == LIBSSH2_ERROR_EAGAIN) { + waitsocket(sock, session); + } + while (libssh2_channel_wait_closed(channel) == LIBSSH2_ERROR_EAGAIN) { + waitsocket(sock, session); + } + + int exit_status = libssh2_channel_get_exit_status(channel); + ESP_LOGI(TAG, "Command exited with status: %d", exit_status); + + libssh2_channel_free(channel); + + libssh2_session_disconnect(session, "Bye"); + libssh2_session_free(session); + libssh2_exit(); + close(sock); + return (exit_status == 0); + +cleanup_session: + libssh2_session_disconnect(session, "Error"); + libssh2_session_free(session); + libssh2_exit(); + close(sock); + return false; +} diff --git a/main/ssh_client.h b/main/ssh_client.h new file mode 100644 index 0000000..9b1bfbb --- /dev/null +++ b/main/ssh_client.h @@ -0,0 +1,25 @@ +#ifndef SSH_CLIENT_H +#define SSH_CLIENT_H + +#include + +/** + * Read the ATECC608B public key, format it as an SSH authorized_keys entry, + * and print it to the console. Call this once at boot so the key can be + * added to the server's authorized_keys file. + * + * Output format: + * ecdsa-sha2-nistp256 keypitecc + */ +void ssh_print_public_key(void); + +/** + * Open a TCP connection to the configured SSH server, authenticate using the + * ATECC608B hardware key, execute @p cmd, log the output, and disconnect. + * + * @param cmd Shell command string to run on the remote host. + * @return true on success, false on any error. + */ +bool ssh_execute_command(const char *cmd); + +#endif /* SSH_CLIENT_H */ diff --git a/main/wifi.c b/main/wifi.c new file mode 100644 index 0000000..5e9143c --- /dev/null +++ b/main/wifi.c @@ -0,0 +1,81 @@ +#include "wifi.h" + +#include "sdkconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "wifi"; + +static EventGroupHandle_t s_wifi_event_group; +static const int WIFI_CONNECTED_BIT = BIT0; +static volatile bool s_connected = false; + +/* ------------------------------------------------------------------------- + * 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) { + s_connected = false; + xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + 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)); + s_connected = true; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +/* ------------------------------------------------------------------------- + * Public API + * ---------------------------------------------------------------------- */ +void wifi_init(void) +{ + s_wifi_event_group = xEventGroupCreate(); + + 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(); + configASSERT(sta_netif); + + ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, "keypitecc")); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + 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)); + + wifi_config_t wifi_config = {}; + strncpy((char *)wifi_config.sta.ssid, CONFIG_WIFI_SSID, + sizeof(wifi_config.sta.ssid) - 1); + strncpy((char *)wifi_config.sta.password, CONFIG_WIFI_PASSWORD, + sizeof(wifi_config.sta.password) - 1); + 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", CONFIG_WIFI_SSID); +} + +bool wifi_is_connected(void) +{ + return s_connected; +} diff --git a/main/wifi.h b/main/wifi.h new file mode 100644 index 0000000..d4ed3df --- /dev/null +++ b/main/wifi.h @@ -0,0 +1,19 @@ +#ifndef WIFI_H +#define WIFI_H + +#include + +/** + * Initialise WiFi in STA mode, connect to the SSID defined in Kconfig, + * and register event handlers. Blocks until a first connection attempt + * is made, but does NOT block forever – reconnection is handled in the + * background. + */ +void wifi_init(void); + +/** + * Returns true when the station has a valid IP address (i.e. WiFi is up). + */ +bool wifi_is_connected(void); + +#endif /* WIFI_H */ diff --git a/main/zk_auth.h b/main/zk_auth.h deleted file mode 100644 index bbb1997..0000000 --- a/main/zk_auth.h +++ /dev/null @@ -1,411 +0,0 @@ -#ifndef ZK_AUTH_H -#define ZK_AUTH_H - -#include "cryptoauthlib.h" -#include "host/atca_host.h" -#include "libssh2_config.h" -#include "mbedtls/sha256.h" -#include "netdb.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -// Added network headers for ESP-IDF (lwIP) sockets -#include -#include -#include -#include - -#define BUFSIZE 3200 - -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; - - // 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) { - 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; - } else { - initialized = true; - printf("\n=== ZK Authentication Initialized ===\n"); - } - return true; - } - - // Marked as static so it can be passed as a C callback to libssh2 - // Helper to safely format SSH integers (mpint) exactly how OpenSSH demands - static uint32_t write_mpint(uint8_t *buf, const uint8_t *val, uint32_t size) { - uint32_t start = 0; - - // Strip unnecessary leading zeros - while (start < (size - 1) && val[start] == 0x00) { - start++; - } - - // If the most significant bit is 1, we must prepend a 0x00 padding byte - bool pad = (val[start] & 0x80) != 0; - uint32_t len = (size - start) + (pad ? 1 : 0); - - uint32_t offset = 0; - buf[offset++] = (len >> 24) & 0xFF; - buf[offset++] = (len >> 16) & 0xFF; - buf[offset++] = (len >> 8) & 0xFF; - buf[offset++] = len & 0xFF; - - if (pad) { - buf[offset++] = 0x00; - } - memcpy(&buf[offset], &val[start], size - start); - return offset + (size - start); - } - - // The main signing callback - static int sign_callback(LIBSSH2_SESSION *session, unsigned char **sig, - size_t *sig_len, const unsigned char *data, - size_t data_len, void **abstract) { - uint8_t digest[32]; - uint8_t raw_sig[64]; - - // 1. Hash the fully assembled challenge buffer provided by libssh2 - mbedtls_sha256(data, data_len, digest, 0); - - // 2. Request signature from the ATECC608B - if (atcab_sign(0x0, digest, raw_sig) != ATCA_SUCCESS) { - printf("CRITICAL: ATECC608B Signing Failed!\n"); - return -1; - } - - // 3. Allocate memory JUST for the two mathematical integers (max ~74 bytes) - unsigned char *buf = (unsigned char *)malloc(80); - if (!buf) - return -1; - - // 4. Format strictly as [mpint R] [mpint S]. NO outer strings! - uint32_t offset = 0; - offset += write_mpint(&buf[offset], &raw_sig[0], 32); // Format R - offset += write_mpint(&buf[offset], &raw_sig[32], 32); // Format S - - // Hand ownership to libssh2 - *sig = buf; - *sig_len = offset; - - return 0; // Success! - } - - // Helper function to write a 32-bit integer in big-endian format - void write_uint32_be(uint8_t *buf, uint32_t val) { - buf[0] = (val >> 24) & 0xFF; - buf[1] = (val >> 16) & 0xFF; - buf[2] = (val >> 8) & 0xFF; - buf[3] = val & 0xFF; - } - - // Function to generate the authorized_keys string - void generate_ssh_authorized_key(const uint8_t *atecc_pubkey, - uint8_t *out_blob) { - uint8_t ssh_blob[104]; - uint32_t offset = 0; - - // 1. Key Type - const char *key_type = "ecdsa-sha2-nistp256"; - write_uint32_be(&ssh_blob[offset], 19); - offset += 4; - memcpy(&ssh_blob[offset], key_type, 19); - offset += 19; - - // 2. Curve Name - const char *curve_name = "nistp256"; - write_uint32_be(&ssh_blob[offset], 8); - offset += 4; - memcpy(&ssh_blob[offset], curve_name, 8); - offset += 8; - - // 3. Public Key (Uncompressed format: 0x04 + 64 bytes X/Y) - write_uint32_be(&ssh_blob[offset], 65); - offset += 4; - ssh_blob[offset++] = 0x04; - memcpy(&ssh_blob[offset], atecc_pubkey, 64); - offset += 64; - - memcpy(out_blob, ssh_blob, 104); - } - - static void my_trace_handler(LIBSSH2_SESSION *session, void *context, - const char *data, size_t length) { - // Print the trace data to the console - printf("LIBSSH2_TRACE: %.*s", (int)length, data); - } - - static int waitsocket(int socket_fd, LIBSSH2_SESSION *session) { - struct timeval timeout; - int rc; - fd_set fd; - fd_set *writefd = NULL; - fd_set *readfd = NULL; - int dir; - - timeout.tv_sec = 10; - timeout.tv_usec = 0; - - FD_ZERO(&fd); - - FD_SET(socket_fd, &fd); - - /* now make sure we wait in the correct direction */ - dir = libssh2_session_block_directions(session); - - if (dir & LIBSSH2_SESSION_BLOCK_INBOUND) - readfd = &fd; - - if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) - writefd = &fd; - - rc = select(socket_fd + 1, readfd, writefd, NULL, &timeout); - - return rc; - } - - // Get device identity (for /api/identity endpoint) - char *get_identity_json() { - uint8_t atecc_pubkey[64]; // 65 bytes * 2 + null - // Get public key from ATECC608B and convert to hex - ATCA_STATUS status = atcab_get_pubkey(0, atecc_pubkey); - if (status != ATCA_SUCCESS) { - printf("Failed to read public key from ATECC608B: 0x%02X\n", status); - } - - uint8_t pubkey_blob[104]; - - generate_ssh_authorized_key(atecc_pubkey, pubkey_blob); - - // 4. Base64 Encode the blob - size_t b64_len = 0; - mbedtls_base64_encode(NULL, 0, &b64_len, pubkey_blob, 104); - unsigned char b64_out[b64_len]; - mbedtls_base64_encode(b64_out, b64_len, &b64_len, pubkey_blob, 104); - printf("ecdsa-sha2-nistp256 %s esp32-atecc608b\n", b64_out); - - // Concat type string with pubkey and identifier for authorized_keys format - char authorized_keys_line[150]; // 19 + 1 + b64_len + 1 + 13 + 1 = 150 - snprintf(authorized_keys_line, sizeof(authorized_keys_line), - "ecdsa-sha2-nistp256 %s esp32-atecc608b", b64_out); - - cJSON *root = cJSON_CreateObject(); - cJSON_AddStringToObject(root, "sshPublicKey", authorized_keys_line); - char *json_str = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - - const char *username = "jonathan"; - - int sock; - struct sockaddr_in sin; - const char *hostname = "192.168.178.86"; - unsigned short port = 22; - void *my_abstract = NULL; - LIBSSH2_SESSION *session; - - ESP_LOGI(TAG, "libssh2_version is %s", LIBSSH2_VERSION); - int rc = libssh2_init(0); - if (rc) { - ESP_LOGE(TAG, "libssh2 initialization failed (%d)", rc); - while (1) { - vTaskDelay(1); - } - } - - ESP_LOGD(TAG, "hostname=%s", hostname); - ESP_LOGD(TAG, "port=%d", port); - sin.sin_family = AF_INET; - // sin.sin_port = htons(22); - sin.sin_port = htons(port); - sin.sin_addr.s_addr = inet_addr(hostname); - ESP_LOGD(TAG, "sin.sin_addr.s_addr=%" PRIx32, sin.sin_addr.s_addr); - if (sin.sin_addr.s_addr == 0xffffffff) { - struct hostent *hp; - hp = gethostbyname(hostname); - if (hp == NULL) { - ESP_LOGE(TAG, "gethostbyname fail"); - ESP_LOGE(TAG, "hostname=%s", hostname); - ESP_LOGE(TAG, "port=%d", port); - while (1) { - vTaskDelay(1); - } - } - struct ip4_addr *ip4_addr; - ip4_addr = (struct ip4_addr *)hp->h_addr; - sin.sin_addr.s_addr = ip4_addr->addr; - ESP_LOGD(TAG, "sin.sin_addr.s_addr=%" PRIx32, sin.sin_addr.s_addr); - } - - sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == -1) { - ESP_LOGE(TAG, "failed to create socket!"); - while (1) { - vTaskDelay(1); - } - } - - if (connect(sock, (struct sockaddr *)(&sin), sizeof(struct sockaddr_in)) != - 0) { - ESP_LOGE(TAG, "failed to connect!"); - ESP_LOGE(TAG, "hostname=%s", hostname); - ESP_LOGE(TAG, "port=%d", port); - while (1) { - vTaskDelay(1); - } - } - - printf("Connected. Starting SSH handshake...\n"); - - session = libssh2_session_init(); - if (!session) { - ESP_LOGE(TAG, "failed to session init"); - while (1) { - vTaskDelay(1); - } - } - - // Enables extensive logging from libssh2 - // Needs debug logging enabled in menuconfig > components > libssh2 - // libssh2_trace(session, ~0); - // libssh2_trace_sethandler(session, NULL, my_trace_handler); - - printf("Session initialized. Setting timeout...\n"); - // libssh2_session_set_timeout(session, 10000); - - printf("Performing handshake...\n"); - rc = libssh2_session_handshake(session, sock); - if (rc) { - ESP_LOGE(TAG, "Failure establishing SSH session: %d", rc); - while (1) { - vTaskDelay(1); - } - } - - printf("Handshake completed. Authenticating with public key...\n"); - - // PUBLIC KEY AUTH: using custom callback - rc = libssh2_userauth_publickey(session, username, pubkey_blob, - sizeof(pubkey_blob), sign_callback, - &my_abstract); - - if (rc == 0) { - printf("SSH authentication successful!\n"); - - // 1. Open a channel within the authenticated session - LIBSSH2_CHANNEL *channel = libssh2_channel_open_session(session); - if (!channel) { - printf("Failed to open a session channel!\n"); - } else { - // 2. Execute the command - const char *cmd = "ls -la"; - printf("Executing command: %s\n", cmd); - - rc = libssh2_channel_exec(channel, cmd); - if (rc != 0) { - printf("Failed to execute command. Error: %d\n", rc); - } else { - printf("--- Command Output ---\n"); - - char buffer[256]; - int bytes_read; - - // 3. Read the output in a loop until the channel closes (EOF) - // libssh2_channel_read returns the amount of bytes read, 0 on EOF, or - // <0 on error - while ((bytes_read = libssh2_channel_read(channel, buffer, - sizeof(buffer) - 1)) > 0) { - buffer[bytes_read] = - '\0'; // Null-terminate the chunk so printf handles it safely - printf("%s", buffer); - } - - if (bytes_read < 0) { - printf("\n[Read failed with error code: %d]\n", bytes_read); - } - printf("\n----------------------\n"); - } - - // 4. Gracefully close the channel and grab the exit code (e.g., 0 for - // success) - libssh2_channel_close(channel); - int exit_status = libssh2_channel_get_exit_status(channel); - printf("Command exited with status: %d\n", exit_status); - - // 5. Free the channel memory - libssh2_channel_free(channel); - } - } else { - printf("Authentication failed\n"); - } - - // Cleanup - libssh2_session_disconnect(session, "Bye"); - libssh2_session_free(session); - close(sock); - libssh2_exit(); - - return json_str; - } -}; - -#endif // ZK_AUTH_H \ No newline at end of file diff --git a/main/zk_handlers.h b/main/zk_handlers.h deleted file mode 100644 index bb9ac47..0000000 --- a/main/zk_handlers.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef ZK_HANDLERS_H -#define ZK_HANDLERS_H - -#include "zk_auth.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Global ZK Auth instance (to be initialized in main) -extern ZKAuth zk_auth; -extern httpd_handle_t server_http; - -// 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 -// Handle CORS preflight -// Register all ZK auth routes to the HTTP server -void register_zk_handlers(httpd_handle_t server) { - 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); -} - -#endif // ZK_HANDLERS_H diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 6048dae..2a819a8 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,9 +1,13 @@ # This file was generated using idf.py save-defconfig. It can be edited manually. # Espressif IoT Development Framework (ESP-IDF) 5.5.2 Project Minimal Configuration # +CONFIG_IDF_TARGET="esp32c6" CONFIG_WIFI_SSID="DoNotSetTheRealValueHere" CONFIG_WIFI_PASSWORD="PutTheRealPassInTheSdkconfigFile" -CONFIG_IDF_TARGET="esp32c6" +CONFIG_SSH_HOSTNAME="192.168.178.1" +CONFIG_SSH_USERNAME="user" +CONFIG_SSH_OPEN_COMMAND="open" +CONFIG_SSH_LOCK_COMMAND="lock" CONFIG_ATECC608A_TCUSTOM=y CONFIG_ATCA_I2C_SDA_PIN=22 CONFIG_ATCA_I2C_SCL_PIN=23