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 <cursoragent@cursor.com>
This commit is contained in:
@@ -47,7 +47,7 @@ constexpr char kTag[] = "NetBootstrap";
|
|||||||
{
|
{
|
||||||
const auto nvsResult = secure_store::initEncryptedStorage();
|
const auto nvsResult = secure_store::initEncryptedStorage();
|
||||||
if (!nvsResult) {
|
if (!nvsResult) {
|
||||||
ESP_LOGE(kTag, "encrypted NVS init failed");
|
ESP_LOGE(kTag, "NVS init failed");
|
||||||
return std::unexpected(NetError::NvsInitFailed);
|
return std::unexpected(NetError::NvsInitFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,16 +151,24 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
{
|
{
|
||||||
auto credsResult = store.loadWifiCredentials();
|
auto credsResult = store.loadWifiCredentials();
|
||||||
if (!credsResult) {
|
if (!credsResult) {
|
||||||
ESP_LOGE(kTag, "stored credentials missing");
|
ESP_LOGE(kTag, "stored credentials missing or unreadable");
|
||||||
return std::unexpected(NetError::CredentialsNotFound);
|
return std::unexpected(NetError::CredentialsNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto& creds = credsResult.value();
|
||||||
|
ESP_LOGI(kTag, "joining SSID %.*s",
|
||||||
|
static_cast<int>(creds.ssid().value().size()),
|
||||||
|
creds.ssid().value().data());
|
||||||
|
|
||||||
esp_netif_create_default_wifi_sta();
|
esp_netif_create_default_wifi_sta();
|
||||||
|
|
||||||
StaClient sta;
|
StaClient sta;
|
||||||
if (auto staResult =
|
if (auto staResult =
|
||||||
sta.connect(credsResult.value(), deviceIdentity.hostname());
|
sta.connect(creds, deviceIdentity.hostname());
|
||||||
!staResult) {
|
!staResult) {
|
||||||
|
ESP_LOGW(kTag, "STA connect failed for SSID %.*s — credentials kept in NVS",
|
||||||
|
static_cast<int>(creds.ssid().value().size()),
|
||||||
|
creds.ssid().value().data());
|
||||||
return std::unexpected(staResult.error());
|
return std::unexpected(staResult.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +215,11 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
if (staResult) {
|
if (staResult) {
|
||||||
return 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,
|
return startSetupMode(store, tuner, audio, bluetooth, stations, integration,
|
||||||
|
|||||||
@@ -648,12 +648,25 @@ esp_err_t wifiPostHandler(httpd_req_t* req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::array<char, 512> body{};
|
std::array<char, 512> body{};
|
||||||
|
const int contentLen = req->content_len;
|
||||||
|
if (contentLen <= 0 || contentLen >= static_cast<int>(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;
|
int received = 0;
|
||||||
while (received < static_cast<int>(body.size()) - 1) {
|
while (received < contentLen) {
|
||||||
const int chunk = httpd_req_recv(req, body.data() + received,
|
const int chunk = httpd_req_recv(req, body.data() + received,
|
||||||
body.size() - 1 - received);
|
contentLen - received);
|
||||||
if (chunk <= 0) {
|
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;
|
received += chunk;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ enum class NvsInitError {
|
|||||||
*
|
*
|
||||||
* @dname initEncryptedStorage
|
* @dname initEncryptedStorage
|
||||||
* @return Ok on success, or NvsInitError describing the failure.
|
* @return Ok on success, or NvsInitError describing the failure.
|
||||||
* @pubstate When CONFIG_NVS_ENCRYPTION is set, nvs_flash_init() uses the
|
* @pubstate Plain NVS by default (encryption off in sdkconfig.defaults).
|
||||||
* nvs_keys partition and flash-encryption key protection per
|
* Erases and retries on layout or encryption-format mismatch.
|
||||||
* ESP-IDF v5.5 security docs. Erases and retries on layout mismatch.
|
* When CONFIG_NVS_ENCRYPTION is set, uses nvs_keys per ESP-IDF docs.
|
||||||
*
|
*
|
||||||
* @author Michele Bigi
|
* @author Michele Bigi
|
||||||
* @date 2026-07-07
|
* @date 2026-07-07
|
||||||
|
|||||||
@@ -22,6 +22,32 @@ namespace secure_store {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr char kTag[] = "NvsPlatformInit";
|
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).
|
* @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");
|
ESP_LOGW(kTag, "NVS encryption without flash encryption — check Kconfig");
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
ESP_LOGW(kTag, "NVS encryption disabled — not for production");
|
ESP_LOGI(kTag, "NVS plain mode (encryption off — current bring-up default)");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +79,15 @@ std::expected<void, NvsInitError> initEncryptedStorage() noexcept
|
|||||||
logEncryptionMode();
|
logEncryptionMode();
|
||||||
|
|
||||||
esp_err_t err = nvs_flash_init();
|
esp_err_t err = nvs_flash_init();
|
||||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES
|
if (err == ESP_OK) {
|
||||||
|| err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
return {};
|
||||||
ESP_LOGW(kTag, "NVS partition needs erase (err=0x%x)", static_cast<unsigned>(err));
|
}
|
||||||
|
|
||||||
|
if (isRecoverableNvsInitError(err)) {
|
||||||
|
ESP_LOGW(kTag,
|
||||||
|
"NVS partition incompatible (0x%x) — erasing (stored Wi-Fi "
|
||||||
|
"and presets will be cleared)",
|
||||||
|
static_cast<unsigned>(err));
|
||||||
err = nvs_flash_erase();
|
err = nvs_flash_erase();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(kTag, "nvs_flash_erase failed (0x%x)", static_cast<unsigned>(err));
|
ESP_LOGE(kTag, "nvs_flash_erase failed (0x%x)", static_cast<unsigned>(err));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include "secure_store/NvsSecureStore.hpp"
|
#include "secure_store/NvsSecureStore.hpp"
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
#include "nvs.h"
|
#include "nvs.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
@@ -32,12 +33,16 @@ constexpr char kSsidKey[] = "wifi_ssid";
|
|||||||
constexpr char kPasswordKey[] = "wifi_pwd";
|
constexpr char kPasswordKey[] = "wifi_pwd";
|
||||||
constexpr char kStationListKey[] = "station_list";
|
constexpr char kStationListKey[] = "station_list";
|
||||||
constexpr char kLastPresetKey[] = "last_preset";
|
constexpr char kLastPresetKey[] = "last_preset";
|
||||||
|
constexpr char kTag[] = "NvsSecureStore";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool NvsSecureStore::hasWifiCredentials() const
|
bool NvsSecureStore::hasWifiCredentials() const
|
||||||
{
|
{
|
||||||
nvs_handle_t handle = 0;
|
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<unsigned>(openErr));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +58,9 @@ std::expected<void, core::StoreError>
|
|||||||
NvsSecureStore::saveWifiCredentials(const core::WifiCredentials& creds)
|
NvsSecureStore::saveWifiCredentials(const core::WifiCredentials& creds)
|
||||||
{
|
{
|
||||||
nvs_handle_t handle = 0;
|
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<unsigned>(openErr));
|
||||||
return std::unexpected(core::StoreError::IoFailed);
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +83,10 @@ NvsSecureStore::saveWifiCredentials(const core::WifiCredentials& creds)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(kTag, "save wifi credentials failed (0x%x)", static_cast<unsigned>(err));
|
||||||
return std::unexpected(core::StoreError::IoFailed);
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
}
|
}
|
||||||
|
ESP_LOGI(kTag, "Wi-Fi credentials saved");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +94,10 @@ std::expected<core::WifiCredentials, core::StoreError>
|
|||||||
NvsSecureStore::loadWifiCredentials() const
|
NvsSecureStore::loadWifiCredentials() const
|
||||||
{
|
{
|
||||||
nvs_handle_t handle = 0;
|
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<unsigned>(openErr));
|
||||||
return std::unexpected(core::StoreError::NotFound);
|
return std::unexpected(core::StoreError::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Flash and NVS encryption (T8)
|
# 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
|
DigiRadio stores Wi-Fi credentials, preset lists, audio profiles, and the last
|
||||||
preset index in the `digiradio` NVS namespace. Firmware **0.8.3+** enables:
|
preset index in the `digiradio` NVS namespace. Firmware **0.8.3+** enables:
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "bluetooth/BluetoothService.hpp"
|
#include "bluetooth/BluetoothService.hpp"
|
||||||
#include "integration/IntegrationService.hpp"
|
#include "integration/IntegrationService.hpp"
|
||||||
#include "net/NetBootstrap.hpp"
|
#include "net/NetBootstrap.hpp"
|
||||||
|
#include "secure_store/NvsPlatformInit.hpp"
|
||||||
#include "ota/OtaService.hpp"
|
#include "ota/OtaService.hpp"
|
||||||
#include "secure_store/NvsSecureStore.hpp"
|
#include "secure_store/NvsSecureStore.hpp"
|
||||||
#include "si4684/Si4684Tuner.hpp"
|
#include "si4684/Si4684Tuner.hpp"
|
||||||
@@ -69,6 +70,11 @@ extern "C" void app_main()
|
|||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (auto nvsResult = secure_store::initEncryptedStorage(); !nvsResult) {
|
||||||
|
ESP_LOGE(kTag, "NVS init failed — halting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
static secure_store::NvsSecureStore store;
|
static secure_store::NvsSecureStore store;
|
||||||
|
|
||||||
static tuner::TunerService tunerService(
|
static tuner::TunerService tunerService(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# DigiRadio — default Kconfig (development / first-board bring-up)
|
# DigiRadio — default Kconfig (development / first-board bring-up)
|
||||||
# Security: NVS + flash encryption — see docs/security-flash-nvs.md
|
# NVS + flash encryption intentionally OFF for Si4684 bring-up (do not re-enable
|
||||||
# Production release mode: sdkconfig.defaults.production (overlay at build time)
|
# 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"
|
CONFIG_IDF_TARGET="esp32s3"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user