Add native Wi-Fi management and persistence

This commit is contained in:
2026-08-23 14:57:53 +02:00
parent aa5fa207b6
commit c5f3595173
11 changed files with 2869 additions and 2 deletions
+439
View File
@@ -0,0 +1,439 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Native ESP-IDF Wi-Fi configuration and non-destructive NVS persistence. */
#include "wifi_config.h"
#include <limits.h>
#include <stddef.h>
#include <string.h>
#include "bootloader_random.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "mbedtls/ctr_drbg.h"
#include "nvs.h"
#include "nvs_flash.h"
_Static_assert(sizeof(wifi_config_sta_profile_t) == WIFI_CONFIG_STA_PROFILE_BLOB_SIZE,
"Wi-Fi STA profile schema size changed");
_Static_assert(offsetof(wifi_app_config_t, profiles) == 112U,
"Wi-Fi config schema offsets changed");
_Static_assert(sizeof(wifi_app_config_t) == WIFI_CONFIG_BLOB_SIZE,
"Wi-Fi config schema size changed");
static SemaphoreHandle_t s_drbg_mutex;
static mbedtls_ctr_drbg_context s_drbg;
static bool s_drbg_ready;
static int early_entropy_source(void *context, unsigned char *output, size_t length)
{
(void)context;
/* This callback is used only during the explicit pre-radio initialization. */
bootloader_random_enable();
esp_fill_random(output, length);
bootloader_random_disable();
return 0;
}
esp_err_t wifi_config_entropy_init(void)
{
static const unsigned char personalization[] = "esp32-serial-sak-wifi";
if (s_drbg_ready) {
return ESP_OK;
}
if (s_drbg_mutex == NULL) {
s_drbg_mutex = xSemaphoreCreateMutex();
if (s_drbg_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
}
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
esp_err_t error = ESP_OK;
if (!s_drbg_ready) {
mbedtls_ctr_drbg_init(&s_drbg);
int result = mbedtls_ctr_drbg_seed(&s_drbg,
early_entropy_source,
NULL,
personalization,
sizeof(personalization) - 1U);
if (result == 0) {
/* Credential generation is rare; never invoke early entropy after RF starts. */
mbedtls_ctr_drbg_set_reseed_interval(&s_drbg, INT_MAX);
s_drbg_ready = true;
} else {
mbedtls_ctr_drbg_free(&s_drbg);
error = ESP_FAIL;
}
}
xSemaphoreGive(s_drbg_mutex);
return error;
}
static esp_err_t fill_credential_random(uint8_t *output, size_t length)
{
if (!s_drbg_ready || output == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
int result = mbedtls_ctr_drbg_random(&s_drbg, output, length);
xSemaphoreGive(s_drbg_mutex);
return result == 0 ? ESP_OK : ESP_FAIL;
}
static bool bytes_are_zero(const uint8_t *data, size_t size)
{
for (size_t i = 0; i < size; ++i) {
if (data[i] != 0U) {
return false;
}
}
return true;
}
static bool unused_bytes_are_zero(const uint8_t *data, size_t used, size_t capacity)
{
return used <= capacity && bytes_are_zero(data + used, capacity - used);
}
static bool psk_is_valid(const uint8_t *psk, uint8_t length)
{
if (length < WIFI_CONFIG_PSK_MIN_LEN || length > WIFI_CONFIG_PSK_MAX_LEN) {
return false;
}
/* ESP-IDF accepts WPA passphrases as 8..63 printable ASCII characters. */
for (uint8_t i = 0; i < length; ++i) {
if (psk[i] < 0x20U || psk[i] > 0x7eU) {
return false;
}
}
return unused_bytes_are_zero(psk, length, WIFI_CONFIG_PSK_MAX_LEN);
}
static bool ap_policy_is_valid(wifi_config_ap_policy_t policy)
{
return policy == WIFI_CONFIG_AP_POLICY_OFF ||
policy == WIFI_CONFIG_AP_POLICY_FALLBACK ||
policy == WIFI_CONFIG_AP_POLICY_ALWAYS;
}
static bool security_is_valid(wifi_config_security_t security)
{
return security == WIFI_CONFIG_SECURITY_MIXED ||
security == WIFI_CONFIG_SECURITY_WPA3;
}
void wifi_config_secure_wipe(void *data, size_t size)
{
volatile uint8_t *byte = (volatile uint8_t *)data;
if (byte == NULL) {
return;
}
while (size-- > 0U) {
*byte++ = 0U;
}
}
esp_err_t wifi_config_defaults(wifi_app_config_t *config)
{
static const uint8_t ap_prefix[] = "ESP32-SAK-";
static const char password_alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static const char hex[] = "0123456789ABCDEF";
uint8_t mac[6] = {0};
uint8_t random_bytes[WIFI_CONFIG_DEFAULT_AP_PSK_LEN] = {0};
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* A zero baseline makes all reserved and unused bytes canonical. */
memset(config, 0, sizeof(*config));
config->schema_version = WIFI_CONFIG_SCHEMA_VERSION;
config->blob_size = WIFI_CONFIG_BLOB_SIZE;
config->enabled_at_boot = 1U;
config->ap_policy = WIFI_CONFIG_AP_POLICY_FALLBACK;
/* Channel 6 is a conventional world-safe default until country settings exist. */
config->ap_channel = 6U;
esp_err_t err = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
if (err != ESP_OK) {
return err;
}
memcpy(config->ap_ssid, ap_prefix, sizeof(ap_prefix) - 1U);
size_t offset = sizeof(ap_prefix) - 1U;
for (size_t i = 3U; i < sizeof(mac); ++i) {
config->ap_ssid[offset++] = (uint8_t)hex[mac[i] >> 4U];
config->ap_ssid[offset++] = (uint8_t)hex[mac[i] & 0x0fU];
}
config->ap_ssid_len = (uint8_t)offset;
/*
* A 64-character alphabet maps six random bits without modulo bias. The
* password is intentionally independent of the public MAC-derived suffix.
*/
err = fill_credential_random(random_bytes, sizeof(random_bytes));
if (err != ESP_OK) {
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
return err;
}
for (size_t i = 0; i < sizeof(random_bytes); ++i) {
config->ap_psk[i] = (uint8_t)password_alphabet[random_bytes[i] & 0x3fU];
}
config->ap_psk_len = WIFI_CONFIG_DEFAULT_AP_PSK_LEN;
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
return wifi_config_validate(config);
}
esp_err_t wifi_config_validate(const wifi_app_config_t *config)
{
if (config == NULL ||
config->schema_version != WIFI_CONFIG_SCHEMA_VERSION ||
config->blob_size != WIFI_CONFIG_BLOB_SIZE ||
config->enabled_at_boot > 1U ||
!ap_policy_is_valid(config->ap_policy) ||
config->ap_ssid_len == 0U ||
config->ap_ssid_len > WIFI_CONFIG_SSID_MAX_LEN ||
!unused_bytes_are_zero(config->ap_ssid, config->ap_ssid_len,
WIFI_CONFIG_SSID_MAX_LEN) ||
!psk_is_valid(config->ap_psk, config->ap_psk_len) ||
config->ap_channel < WIFI_CONFIG_AP_CHANNEL_MIN ||
config->ap_channel > WIFI_CONFIG_AP_CHANNEL_MAX ||
!bytes_are_zero(config->reserved, sizeof(config->reserved)) ||
!bytes_are_zero(config->reserved_tail, sizeof(config->reserved_tail))) {
return ESP_ERR_INVALID_ARG;
}
for (size_t i = 0; i < WIFI_CONFIG_STA_PROFILE_COUNT; ++i) {
const wifi_config_sta_profile_t *profile = &config->profiles[i];
if (profile->enabled > 1U ||
!security_is_valid(profile->security) ||
!bytes_are_zero(profile->reserved, sizeof(profile->reserved)) ||
!bytes_are_zero(profile->reserved_tail, sizeof(profile->reserved_tail))) {
return ESP_ERR_INVALID_ARG;
}
bool empty = profile->ssid_len == 0U && profile->psk_len == 0U;
if (empty) {
if (profile->enabled != 0U ||
!bytes_are_zero(profile->ssid, sizeof(profile->ssid)) ||
!bytes_are_zero(profile->psk, sizeof(profile->psk))) {
return ESP_ERR_INVALID_ARG;
}
continue;
}
/* A disabled SSID may be staged before its no-echo secret is entered. */
if (profile->ssid_len == 0U ||
profile->ssid_len > WIFI_CONFIG_SSID_MAX_LEN ||
!unused_bytes_are_zero(profile->ssid, profile->ssid_len,
WIFI_CONFIG_SSID_MAX_LEN)) {
return ESP_ERR_INVALID_ARG;
}
if (profile->psk_len == 0U) {
if (profile->enabled != 0U ||
!bytes_are_zero(profile->psk, sizeof(profile->psk))) {
return ESP_ERR_INVALID_ARG;
}
} else if (!psk_is_valid(profile->psk, profile->psk_len)) {
return ESP_ERR_INVALID_ARG;
}
}
return ESP_OK;
}
bool wifi_config_parse_ap_policy(const char *text, wifi_config_ap_policy_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "off") == 0) {
*value = WIFI_CONFIG_AP_POLICY_OFF;
return true;
}
if (strcmp(text, "fallback") == 0) {
*value = WIFI_CONFIG_AP_POLICY_FALLBACK;
return true;
}
if (strcmp(text, "always") == 0) {
*value = WIFI_CONFIG_AP_POLICY_ALWAYS;
return true;
}
return false;
}
const char *wifi_config_ap_policy_to_string(wifi_config_ap_policy_t value)
{
switch (value) {
case WIFI_CONFIG_AP_POLICY_OFF:
return "off";
case WIFI_CONFIG_AP_POLICY_FALLBACK:
return "fallback";
case WIFI_CONFIG_AP_POLICY_ALWAYS:
return "always";
default:
return NULL;
}
}
bool wifi_config_parse_security(const char *text, wifi_config_security_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "mixed") == 0) {
*value = WIFI_CONFIG_SECURITY_MIXED;
return true;
}
if (strcmp(text, "wpa3") == 0) {
*value = WIFI_CONFIG_SECURITY_WPA3;
return true;
}
return false;
}
const char *wifi_config_security_to_string(wifi_config_security_t value)
{
switch (value) {
case WIFI_CONFIG_SECURITY_MIXED:
return "mixed";
case WIFI_CONFIG_SECURITY_WPA3:
return "wpa3";
default:
return NULL;
}
}
esp_err_t wifi_config_storage_init(void)
{
/* Never erase shared NVS to recover an error; report it to the caller. */
return nvs_flash_init();
}
esp_err_t wifi_config_load(wifi_app_config_t *config,
wifi_config_load_source_t *source)
{
if (config == NULL || source == NULL) {
return ESP_ERR_INVALID_ARG;
}
*source = WIFI_CONFIG_LOAD_GENERATED_MISSING;
/* Defaults are established before any NVS access, including failure paths. */
esp_err_t err = wifi_config_defaults(config);
if (err != ESP_OK) {
return err;
}
err = wifi_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(WIFI_CONFIG_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
size_t stored_size = 0U;
err = nvs_get_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, NULL, &stored_size);
if (err == ESP_ERR_NVS_NOT_FOUND) {
nvs_close(handle);
return ESP_OK;
}
if (err == ESP_ERR_NVS_TYPE_MISMATCH) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
nvs_close(handle);
return ESP_OK;
}
if (err != ESP_OK) {
nvs_close(handle);
return err;
}
/* A different size is a different schema, even if its prefix looks valid. */
if (stored_size != WIFI_CONFIG_BLOB_SIZE) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
nvs_close(handle);
return ESP_OK;
}
wifi_app_config_t stored_config;
memset(&stored_config, 0, sizeof(stored_config));
err = nvs_get_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, &stored_config, &stored_size);
nvs_close(handle);
if (err == ESP_ERR_NVS_INVALID_LENGTH) {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return ESP_OK;
}
if (err != ESP_OK) {
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return err;
}
if (stored_size == WIFI_CONFIG_BLOB_SIZE &&
wifi_config_validate(&stored_config) == ESP_OK) {
*config = stored_config;
*source = WIFI_CONFIG_LOAD_STORED;
} else {
*source = WIFI_CONFIG_LOAD_GENERATED_INVALID;
}
wifi_config_secure_wipe(&stored_config, sizeof(stored_config));
return ESP_OK;
}
esp_err_t wifi_config_save(const wifi_app_config_t *config)
{
esp_err_t err = wifi_config_validate(config);
if (err != ESP_OK) {
return err;
}
err = wifi_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(WIFI_CONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_set_blob(handle, WIFI_CONFIG_NVS_BLOB_KEY, config,
WIFI_CONFIG_BLOB_SIZE);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults)
{
if (defaults != NULL) {
return wifi_config_save(defaults);
}
wifi_app_config_t generated_defaults;
esp_err_t err = wifi_config_defaults(&generated_defaults);
if (err == ESP_OK) {
err = wifi_config_save(&generated_defaults);
}
wifi_config_secure_wipe(&generated_defaults, sizeof(generated_defaults));
return err;
}