/* SPDX-License-Identifier: GPL-3.0-only */ /* Native ESP-IDF Wi-Fi configuration and non-destructive NVS persistence. */ #include "wifi_config.h" #include #include #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; }