369 lines
11 KiB
C
369 lines
11 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Native ESP-IDF Wi-Fi configuration and non-destructive NVS persistence. */
|
|
|
|
#include "wifi_config.h"
|
|
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
|
|
#include "esp_mac.h"
|
|
#include "nvs.h"
|
|
#include "nvs_flash.h"
|
|
#include "secure_random.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 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)
|
|
{
|
|
/* Keep the existing Wi-Fi API while using the device-wide wipe primitive. */
|
|
secure_wipe(data, size);
|
|
}
|
|
|
|
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 = secure_random_fill(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;
|
|
}
|