From 7d3369b0984569243c360481d3a8854addb2bb4f Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Wed, 5 Aug 2026 14:10:00 +0200 Subject: [PATCH] Fix Wi-Fi credential save with plain NVS (encryption off). Initialise NVS before integration startup, recover from encrypted-partition mismatches without re-enabling encryption, and improve provisioning logs and POST body handling. Co-authored-by: Cursor --- Software/components/net/src/NetBootstrap.cpp | 20 ++++++++-- .../components/net/src/SetupWebServer.cpp | 19 +++++++-- .../include/secure_store/NvsPlatformInit.hpp | 6 +-- .../secure_store/src/NvsPlatformInit.cpp | 40 +++++++++++++++++-- .../secure_store/src/NvsSecureStore.cpp | 18 +++++++-- Software/docs/security-flash-nvs.md | 7 ++++ Software/main/main.cpp | 6 +++ Software/sdkconfig.defaults | 5 ++- 8 files changed, 102 insertions(+), 19 deletions(-) diff --git a/Software/components/net/src/NetBootstrap.cpp b/Software/components/net/src/NetBootstrap.cpp index 8a19c10..c39b11d 100644 --- a/Software/components/net/src/NetBootstrap.cpp +++ b/Software/components/net/src/NetBootstrap.cpp @@ -47,7 +47,7 @@ constexpr char kTag[] = "NetBootstrap"; { const auto nvsResult = secure_store::initEncryptedStorage(); if (!nvsResult) { - ESP_LOGE(kTag, "encrypted NVS init failed"); + ESP_LOGE(kTag, "NVS init failed"); return std::unexpected(NetError::NvsInitFailed); } @@ -151,16 +151,24 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner, { auto credsResult = store.loadWifiCredentials(); if (!credsResult) { - ESP_LOGE(kTag, "stored credentials missing"); + ESP_LOGE(kTag, "stored credentials missing or unreadable"); return std::unexpected(NetError::CredentialsNotFound); } + const auto& creds = credsResult.value(); + ESP_LOGI(kTag, "joining SSID %.*s", + static_cast(creds.ssid().value().size()), + creds.ssid().value().data()); + esp_netif_create_default_wifi_sta(); StaClient sta; if (auto staResult = - sta.connect(credsResult.value(), deviceIdentity.hostname()); + sta.connect(creds, deviceIdentity.hostname()); !staResult) { + ESP_LOGW(kTag, "STA connect failed for SSID %.*s — credentials kept in NVS", + static_cast(creds.ssid().value().size()), + creds.ssid().value().data()); return std::unexpected(staResult.error()); } @@ -207,7 +215,11 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner, if (staResult) { return staResult; } - ESP_LOGW(kTag, "STA join failed — falling back to setup SoftAP"); + ESP_LOGW(kTag, + "STA join failed — Wi-Fi credentials are stored; check " + "SSID/password or signal (falling back to setup SoftAP)"); + } else { + ESP_LOGI(kTag, "no stored Wi-Fi credentials — entering setup SoftAP"); } return startSetupMode(store, tuner, audio, bluetooth, stations, integration, diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index e3155e8..9f1da4d 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -648,12 +648,25 @@ esp_err_t wifiPostHandler(httpd_req_t* req) } std::array body{}; + const int contentLen = req->content_len; + if (contentLen <= 0 || contentLen >= static_cast(body.size())) { + const std::string json = + core::serializeWifiProvisionErrorJson("invalid_json"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + int received = 0; - while (received < static_cast(body.size()) - 1) { + while (received < contentLen) { const int chunk = httpd_req_recv(req, body.data() + received, - body.size() - 1 - received); + contentLen - received); if (chunk <= 0) { - break; + const std::string json = + core::serializeWifiProvisionErrorJson("invalid_json"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); } received += chunk; } diff --git a/Software/components/secure_store/include/secure_store/NvsPlatformInit.hpp b/Software/components/secure_store/include/secure_store/NvsPlatformInit.hpp index 8fda773..d79062d 100644 --- a/Software/components/secure_store/include/secure_store/NvsPlatformInit.hpp +++ b/Software/components/secure_store/include/secure_store/NvsPlatformInit.hpp @@ -36,9 +36,9 @@ enum class NvsInitError { * * @dname initEncryptedStorage * @return Ok on success, or NvsInitError describing the failure. - * @pubstate When CONFIG_NVS_ENCRYPTION is set, nvs_flash_init() uses the - * nvs_keys partition and flash-encryption key protection per - * ESP-IDF v5.5 security docs. Erases and retries on layout mismatch. + * @pubstate Plain NVS by default (encryption off in sdkconfig.defaults). + * Erases and retries on layout or encryption-format mismatch. + * When CONFIG_NVS_ENCRYPTION is set, uses nvs_keys per ESP-IDF docs. * * @author Michele Bigi * @date 2026-07-07 diff --git a/Software/components/secure_store/src/NvsPlatformInit.cpp b/Software/components/secure_store/src/NvsPlatformInit.cpp index de8a627..54c83d9 100644 --- a/Software/components/secure_store/src/NvsPlatformInit.cpp +++ b/Software/components/secure_store/src/NvsPlatformInit.cpp @@ -22,6 +22,32 @@ namespace secure_store { namespace { constexpr char kTag[] = "NvsPlatformInit"; +/** + * @brief isRecoverableNvsInitError — true when erase + re-init may help. + * + * @dname isRecoverableNvsInitError + * @param err Return code from nvs_flash_init(). + * @return true for layout/encryption mismatch errors. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-08-05 + */ +[[nodiscard]] bool isRecoverableNvsInitError(esp_err_t err) noexcept +{ + if (err == ESP_ERR_NVS_NO_FREE_PAGES + || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + return true; + } +#if !CONFIG_NVS_ENCRYPTION + // Plain-NVS build after encrypted-NVS flash: erase once to migrate. + return err == ESP_ERR_NVS_WRONG_ENCRYPTION + || err == ESP_ERR_NVS_CORRUPT_KEY_PART; +#else + return err == ESP_ERR_NVS_CORRUPT_KEY_PART; +#endif +} + /** * @brief logEncryptionMode — log active NVS security Kconfig (no secrets). * @@ -42,7 +68,7 @@ void logEncryptionMode() noexcept ESP_LOGW(kTag, "NVS encryption without flash encryption — check Kconfig"); #endif #else - ESP_LOGW(kTag, "NVS encryption disabled — not for production"); + ESP_LOGI(kTag, "NVS plain mode (encryption off — current bring-up default)"); #endif } @@ -53,9 +79,15 @@ std::expected initEncryptedStorage() noexcept logEncryptionMode(); esp_err_t err = nvs_flash_init(); - if (err == ESP_ERR_NVS_NO_FREE_PAGES - || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_LOGW(kTag, "NVS partition needs erase (err=0x%x)", static_cast(err)); + if (err == ESP_OK) { + return {}; + } + + if (isRecoverableNvsInitError(err)) { + ESP_LOGW(kTag, + "NVS partition incompatible (0x%x) — erasing (stored Wi-Fi " + "and presets will be cleared)", + static_cast(err)); err = nvs_flash_erase(); if (err != ESP_OK) { ESP_LOGE(kTag, "nvs_flash_erase failed (0x%x)", static_cast(err)); diff --git a/Software/components/secure_store/src/NvsSecureStore.cpp b/Software/components/secure_store/src/NvsSecureStore.cpp index f495ca2..8048435 100644 --- a/Software/components/secure_store/src/NvsSecureStore.cpp +++ b/Software/components/secure_store/src/NvsSecureStore.cpp @@ -18,6 +18,7 @@ #include "secure_store/NvsSecureStore.hpp" +#include "esp_log.h" #include "nvs.h" #include "nvs_flash.h" @@ -32,12 +33,16 @@ constexpr char kSsidKey[] = "wifi_ssid"; constexpr char kPasswordKey[] = "wifi_pwd"; constexpr char kStationListKey[] = "station_list"; constexpr char kLastPresetKey[] = "last_preset"; +constexpr char kTag[] = "NvsSecureStore"; } // namespace bool NvsSecureStore::hasWifiCredentials() const { nvs_handle_t handle = 0; - if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) { + const esp_err_t openErr = nvs_open(kNamespace, NVS_READONLY, &handle); + if (openErr != ESP_OK) { + ESP_LOGW(kTag, "nvs_open failed in hasWifiCredentials (0x%x)", + static_cast(openErr)); return false; } @@ -53,7 +58,9 @@ std::expected NvsSecureStore::saveWifiCredentials(const core::WifiCredentials& creds) { nvs_handle_t handle = 0; - if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) { + const esp_err_t openErr = nvs_open(kNamespace, NVS_READWRITE, &handle); + if (openErr != ESP_OK) { + ESP_LOGE(kTag, "nvs_open failed (0x%x)", static_cast(openErr)); return std::unexpected(core::StoreError::IoFailed); } @@ -76,8 +83,10 @@ NvsSecureStore::saveWifiCredentials(const core::WifiCredentials& creds) } if (err != ESP_OK) { + ESP_LOGE(kTag, "save wifi credentials failed (0x%x)", static_cast(err)); return std::unexpected(core::StoreError::IoFailed); } + ESP_LOGI(kTag, "Wi-Fi credentials saved"); return {}; } @@ -85,7 +94,10 @@ std::expected NvsSecureStore::loadWifiCredentials() const { nvs_handle_t handle = 0; - if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) { + const esp_err_t openErr = nvs_open(kNamespace, NVS_READONLY, &handle); + if (openErr != ESP_OK) { + ESP_LOGW(kTag, "nvs_open failed in loadWifiCredentials (0x%x)", + static_cast(openErr)); return std::unexpected(core::StoreError::NotFound); } diff --git a/Software/docs/security-flash-nvs.md b/Software/docs/security-flash-nvs.md index 4a988d0..6b62618 100644 --- a/Software/docs/security-flash-nvs.md +++ b/Software/docs/security-flash-nvs.md @@ -1,5 +1,12 @@ # Flash and NVS encryption (T8) +> **Current bring-up (2026):** `sdkconfig.defaults` keeps +> `CONFIG_NVS_ENCRYPTION` and `CONFIG_SECURE_FLASH_ENC_ENABLED` **disabled** +> for Si4684 board validation. Wi-Fi credentials are stored in **plain NVS**. +> After flashing firmware that previously used encrypted NVS, run +> `idf.py erase-flash flash` once so the partition format matches (or rely on +> automatic NVS erase on `ESP_ERR_NVS_WRONG_ENCRYPTION` at boot). + DigiRadio stores Wi-Fi credentials, preset lists, audio profiles, and the last preset index in the `digiradio` NVS namespace. Firmware **0.8.3+** enables: diff --git a/Software/main/main.cpp b/Software/main/main.cpp index e2e0d56..9e9a0f9 100644 --- a/Software/main/main.cpp +++ b/Software/main/main.cpp @@ -15,6 +15,7 @@ #include "bluetooth/BluetoothService.hpp" #include "integration/IntegrationService.hpp" #include "net/NetBootstrap.hpp" +#include "secure_store/NvsPlatformInit.hpp" #include "ota/OtaService.hpp" #include "secure_store/NvsSecureStore.hpp" #include "si4684/Si4684Tuner.hpp" @@ -69,6 +70,11 @@ extern "C" void app_main() return; #endif + if (auto nvsResult = secure_store::initEncryptedStorage(); !nvsResult) { + ESP_LOGE(kTag, "NVS init failed — halting"); + return; + } + static secure_store::NvsSecureStore store; static tuner::TunerService tunerService( diff --git a/Software/sdkconfig.defaults b/Software/sdkconfig.defaults index 2342860..bac4e06 100644 --- a/Software/sdkconfig.defaults +++ b/Software/sdkconfig.defaults @@ -1,6 +1,7 @@ # DigiRadio — default Kconfig (development / first-board bring-up) -# Security: NVS + flash encryption — see docs/security-flash-nvs.md -# Production release mode: sdkconfig.defaults.production (overlay at build time) +# NVS + flash encryption intentionally OFF for Si4684 bring-up (do not re-enable +# without idf.py erase-flash and docs/security-flash-nvs.md migration). +# Production overlay: sdkconfig.defaults.production (not used on current boards). CONFIG_IDF_TARGET="esp32s3"