/** * @file provision.c * @brief HTTP Provisioning */ #include #include #include "provision.h" #include "config.h" #include "crypto.h" #include "esp_log.h" #include "esp_http_client.h" #include "cJSON.h" #include "mbedtls/base64.h" static const char *TAG = "nexus_provision"; /* Retry config */ #define MAX_RETRIES 3 #define CONNECT_TIMEOUT_MS 10000 #define READ_TIMEOUT_MS 30000 /* Config key name for decryption (must match server) */ #define CONFIG_NEXUS_SALT CONFIG_NEXUS_SERVER_SALT /** * @brief Helper: copy cJSON string to char buffer with bounds check */ static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len) { cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key); if (cJSON_IsString(item) && item->valuestring != NULL) { strncpy(buf, item->valuestring, buf_len - 1); buf[buf_len - 1] = '\0'; } } /** * @brief HTTP event handler for provisioning */ static esp_err_t provision_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ERROR: ESP_LOGD(TAG, "HTTP_EVENT_ERROR"); break; case HTTP_EVENT_ON_CONNECTED: ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED"); break; case HTTP_EVENT_ON_HEADER: break; case HTTP_EVENT_ON_DATA: ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); break; case HTTP_EVENT_ON_FINISH: ESP_LOGD(TAG, "HTTP_EVENT_FINISHED"); break; case HTTP_EVENT_DISCONNECTED: ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED"); break; default: break; } return ESP_OK; } esp_err_t provision_device(const char *server_url, const char *token, const char *hwid, wg_config_t *config) { if (server_url == NULL || token == NULL || hwid == NULL || config == NULL) { return ESP_ERR_INVALID_ARG; } ESP_LOGI(TAG, "Starting provisioning..."); /* Build request JSON */ cJSON *req_json = cJSON_CreateObject(); cJSON_AddStringToObject(req_json, "token", token); cJSON_AddStringToObject(req_json, "hwid", hwid); char *req_body = cJSON_PrintUnformatted(req_json); cJSON_Delete(req_json); if (req_body == NULL) { ESP_LOGE(TAG, "Failed to create request JSON"); return ESP_ERR_NO_MEM; } /* Build URL */ char url[192]; snprintf(url, sizeof(url), "%s/api/v1/provision", server_url); esp_err_t final_err = ESP_FAIL; for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { ESP_LOGI(TAG, "Provisioning attempt %d/%d...", attempt, MAX_RETRIES); /* Configure HTTP client */ esp_http_client_config_t http_config = { .url = url, .event_handler = provision_event_handler, .timeout_ms = CONNECT_TIMEOUT_MS, .transport_type = HTTP_TRANSPORT_OVER_SSL, .skip_cert_common_name_check = true, /* TODO: use proper CA cert */ }; esp_http_client_handle_t client = esp_http_client_init(&http_config); if (client == NULL) { ESP_LOGE(TAG, "Failed to init HTTP client"); continue; } /* Set POST data */ esp_http_client_set_method(client, HTTP_METHOD_POST); esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_post_field(client, req_body, strlen(req_body)); /* Execute request */ esp_err_t err = esp_http_client_open(client, strlen(req_body)); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } int written = esp_http_client_write(client, req_body, strlen(req_body)); if (written < 0) { ESP_LOGE(TAG, "HTTP write failed"); esp_http_client_close(client); esp_http_client_cleanup(client); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } int content_length = esp_http_client_fetch_headers(client); int status_code = esp_http_client_get_status_code(client); if (status_code != 200) { ESP_LOGE(TAG, "Provisioning failed, HTTP %d", status_code); esp_http_client_close(client); esp_http_client_cleanup(client); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } /* Read response body */ char *response_buf = malloc(content_length + 1); if (response_buf == NULL) { ESP_LOGE(TAG, "Failed to allocate response buffer"); esp_http_client_close(client); esp_http_client_cleanup(client); final_err = ESP_ERR_NO_MEM; break; } int read_len = esp_http_client_read(client, response_buf, content_length); esp_http_client_close(client); esp_http_client_cleanup(client); if (read_len < 0) { ESP_LOGE(TAG, "HTTP read failed"); free(response_buf); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } response_buf[read_len] = '\0'; /* Parse response JSON */ cJSON *resp_json = cJSON_Parse(response_buf); free(response_buf); if (resp_json == NULL) { ESP_LOGE(TAG, "Failed to parse response JSON"); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } /* Extract encrypted_config (base64) */ cJSON *encrypted_item = cJSON_GetObjectItemCaseSensitive(resp_json, "encrypted_config"); if (!cJSON_IsString(encrypted_item) || encrypted_item->valuestring == NULL) { ESP_LOGE(TAG, "Response missing 'encrypted_config'"); cJSON_Delete(resp_json); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } const char *b64_data = encrypted_item->valuestring; size_t b64_len = strlen(b64_data); /* Base64 decode */ size_t decoded_max_len = (b64_len * 3) / 4 + 4; uint8_t *decoded_buf = malloc(decoded_max_len); if (decoded_buf == NULL) { ESP_LOGE(TAG, "Failed to allocate base64 decode buffer"); cJSON_Delete(resp_json); final_err = ESP_ERR_NO_MEM; break; } size_t decoded_len = 0; int b64_ret = mbedtls_base64_decode(decoded_buf, decoded_max_len, &decoded_len, (const unsigned char *)b64_data, b64_len); if (b64_ret != 0) { ESP_LOGE(TAG, "Base64 decode failed: -0x%04x", -b64_ret); free(decoded_buf); cJSON_Delete(resp_json); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } /* Derive decryption key: SHA256(hwid + salt) */ uint8_t key[32]; err = crypto_derive_key(hwid, CONFIG_NEXUS_SALT, key); if (err != ESP_OK) { ESP_LOGE(TAG, "Key derivation failed"); explicit_bzero(key, sizeof(key)); free(decoded_buf); cJSON_Delete(resp_json); final_err = err; break; } /* Decrypt config */ uint8_t *plaintext = NULL; size_t plaintext_len = 0; err = crypto_decrypt(decoded_buf, decoded_len, key, &plaintext, &plaintext_len); explicit_bzero(key, sizeof(key)); free(decoded_buf); if (err != ESP_OK) { ESP_LOGE(TAG, "Decryption failed"); cJSON_Delete(resp_json); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } /* Parse decrypted JSON payload */ cJSON *config_json = cJSON_Parse((char *)plaintext); free(plaintext); if (config_json == NULL) { ESP_LOGE(TAG, "Failed to parse decrypted config JSON"); cJSON_Delete(resp_json); vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); continue; } /* Extract config fields */ copy_json_str(config_json, "device_id", config->device_id, sizeof(config->device_id)); copy_json_str(config_json, "private_key", config->private_key, sizeof(config->private_key)); copy_json_str(config_json, "preshared_key", config->preshared_key, sizeof(config->preshared_key)); copy_json_str(config_json, "internal_ip", config->internal_ip, sizeof(config->internal_ip)); copy_json_str(config_json, "server_pub", config->server_pub, sizeof(config->server_pub)); copy_json_str(config_json, "endpoint", config->endpoint, sizeof(config->endpoint)); copy_json_str(config_json, "dns", config->dns, sizeof(config->dns)); copy_json_str(config_json, "allowed_ips", config->allowed_ips, sizeof(config->allowed_ips)); copy_json_str(config_json, "server_wg_ip", config->server_wg_ip, sizeof(config->server_wg_ip)); copy_json_str(config_json, "config_hash", config->config_hash, sizeof(config->config_hash)); cJSON_Delete(config_json); cJSON_Delete(resp_json); /* Save config to NVS */ err = config_save(config); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err)); final_err = err; break; } ESP_LOGI(TAG, "Provisioning complete, device: %s", config->device_id); final_err = ESP_OK; break; } free(req_body); return final_err; }