Files
ESP32_Serial_Swiss_Army_Knife/src/wifi_config.c
T

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;
}