Blame | Last modification | View Log | RSS feed
#include "CredentialManager.h"#include "SerialLog.h"CredentialManager::CredentialManager() : m_efuse_initialized(false) {memset(m_encryption_key, 0, ENCRYPTION_KEY_SIZE);// Default global options (hostname only)memset(m_hostname, 0, sizeof(m_hostname));#if defined(ESP8266)m_filesystem = nullptr;#endif}CredentialManager::~CredentialManager() {// Clean key from memorymemset(m_encryption_key, 0, ENCRYPTION_KEY_SIZE);clearAll();}bool CredentialManager::begin() {initializeEncryptionKey();return true;}void CredentialManager::initializeEncryptionKey() {#if defined(ESP32)esp_err_t err = esp_efuse_read_block(EFUSE_BLK_KEY0, m_encryption_key, 0,ENCRYPTION_KEY_SIZE * 8);if (err == ESP_OK) {// Check if truly programmed (not all 0xFF)bool all_ff = true;for (int i = 0; i < ENCRYPTION_KEY_SIZE; i++) {if (m_encryption_key[i] != 0xFF) {all_ff = false;break;}}if (!all_ff) {m_efuse_initialized = true;log_info("BLOCK_KEY0 initialized from eFuse - SECURE mode");} else {log_info("BLOCK_KEY0 not programmed - using fallback (NOT SECURE)");m_efuse_initialized = false;// Use fallback key (derived from MAC address or constant)// This obfuscates but is not real securitymemset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);}} else {log_error("Failed to read BLOCK_KEY0: %s", esp_err_to_name(err));// Fallback to predefined keymemset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);}#else// For ESP8266 or other chips, use fallbackmemset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);#endif}String CredentialManager::getStatus() const {if (m_efuse_initialized) {return "SECURE (BLOCK_KEY0 programmed)";} else {return "INSECURE (BLOCK_KEY0 not programmed - fallback key used)";}}void CredentialManager::setHostname(const char* hostname) {if (!hostname) {memset(m_hostname, 0, sizeof(m_hostname));return;}strlcpy(m_hostname, hostname, sizeof(m_hostname));}String CredentialManager::getHostname() const {return String(m_hostname);}// Web server port is fixed at construction time in AsyncFsWebServer// and is not stored as a shared, mutable option in CredentialManager.bool CredentialManager::addCredential(const WiFiCredential& credential, const char* plaintext_password) {if (strlen(credential.ssid) == 0) {log_error("Invalid SSID");return false;}if (!plaintext_password || strlen(plaintext_password) == 0) {log_error("Invalid password");return false;}if (m_credentials.size() >= MAX_CREDENTIALS) {log_error("Maximum credentials reached (%d)", MAX_CREDENTIALS);return false;}if (strlen(plaintext_password) > 63) {log_error("Password too long (max 63 characters)");return false;}// Create credential copy with encrypted passwordWiFiCredential cred = credential;// Encrypt passworduint16_t encrypted_len = 0;if (!encryptPassword(plaintext_password, cred.pwd_encrypted, encrypted_len)) {log_error("Failed to encrypt password");return false;}cred.pwd_len = encrypted_len;m_credentials.push_back(cred);log_debug("Credential added: %s.", cred.ssid);return true;}bool CredentialManager::removeCredential(uint8_t index) {if (index >= m_credentials.size()) {log_error("Invalid credential index %d", index);return false;}log_debug("Removing credential: %s", m_credentials[index].ssid);memset(m_credentials[index].pwd_encrypted, 0, sizeof(m_credentials[index].pwd_encrypted));memset(m_credentials[index].ssid, 0, sizeof(m_credentials[index].ssid));m_credentials.erase(m_credentials.begin() + index);return true;}bool CredentialManager::removeCredential(const char* ssid) {if (!ssid) {return false;}for (size_t i = 0; i < m_credentials.size(); i++) {if (strcmp(m_credentials[i].ssid, ssid) == 0) {return removeCredential(static_cast<uint8_t>(i));}}log_error("Credential not found: %s", ssid);return false;}bool CredentialManager::updateCredential(const WiFiCredential& credential, const char* plaintext_password) {uint8_t index = 255;for (size_t i = 0; i < m_credentials.size(); i++) {if (strcmp(m_credentials[i].ssid, credential.ssid) == 0) {index = i;break;}}if (index >= m_credentials.size()) {log_error("Credential not found: %s", credential.ssid);return false;}if (!plaintext_password || strlen(plaintext_password) == 0) {log_error("Invalid password");return false;}if (strlen(plaintext_password) > 63) {log_error("Password too long (max 63 characters)");return false;}WiFiCredential &cred = m_credentials[index];// Encrypt new passworduint16_t encrypted_len = 0;if (!encryptPassword(plaintext_password, cred.pwd_encrypted, encrypted_len)) {log_error("Failed to encrypt password");return false;}cred.pwd_len = encrypted_len;// Update IP configuration from credential parametercred.gateway = credential.gateway;cred.subnet = credential.subnet;cred.local_ip = credential.local_ip;cred.dns1 = credential.dns1;cred.dns2 = credential.dns2;log_debug("Credential updated: %s.", credential.ssid);return true;}const char *CredentialManager::getSSID(uint8_t index) const {if (index >= m_credentials.size()) {return nullptr;}return m_credentials[index].ssid;}String CredentialManager::getPassword(uint8_t index) {if (index >= m_credentials.size()) {return "";}const WiFiCredential &cred = m_credentials[index];char plaintext[65] = {0};if (decryptPassword(cred.pwd_encrypted, cred.pwd_len, plaintext,64)) {String result = plaintext;// Clean password from memorymemset(plaintext, 0, 65);return result;}return "";}bool CredentialManager::setIPConfiguration(uint8_t index, IPAddress ip, IPAddress gateway, IPAddress subnet) {if (index >= m_credentials.size()) {log_error("Invalid credential index %d", index);return false;}m_credentials[index].local_ip = ip;m_credentials[index].gateway = gateway;m_credentials[index].subnet = subnet;log_debug("IP configuration updated for credential %d: IP=%s, GW=%s, SN=%s",index, ip.toString().c_str(), gateway.toString().c_str(), subnet.toString().c_str());return true;}bool CredentialManager::getIPConfiguration(uint8_t index, IPAddress& ip, IPAddress& gateway, IPAddress& subnet) const {if (index >= m_credentials.size()) {log_error("Invalid credential index %d", index);return false;}ip = m_credentials[index].local_ip;gateway = m_credentials[index].gateway;subnet = m_credentials[index].subnet;return true;}void CredentialManager::clearAll() {// Clean each credentialfor (auto &cred : m_credentials) {memset(cred.pwd_encrypted, 0, sizeof(cred.pwd_encrypted));memset(cred.ssid, 0, sizeof(cred.ssid));}m_credentials.clear();#if defined(ESP8266)saveToFS();#elif defined(ESP32)saveToNVS();#endiflog_debug("All credentials cleared");}bool CredentialManager::encryptPassword(const char *plaintext, uint8_t *ciphertext, uint16_t &cipher_len) {if (!plaintext || !ciphertext) {return false;}size_t plaintext_len = strlen(plaintext);if (plaintext_len > 63) {log_error("Password too long");return false;}// Apply PKCS7 paddinguint8_t *padded = new uint8_t[64];uint16_t padded_len = 0;applyPKCS7Padding((uint8_t *)plaintext, plaintext_len, padded, padded_len);// AES-256-CBC encryptionmbedtls_aes_context aes_ctx;mbedtls_aes_init(&aes_ctx);int ret = mbedtls_aes_setkey_enc(&aes_ctx, m_encryption_key, 256);if (ret != 0) {log_error("AES setkey failed: %d", ret);mbedtls_aes_free(&aes_ctx);delete[] padded;return false;}// Fixed IV (deterministic, not randomized)// In case of randomized IV, save it in the first blockuint8_t iv[AES_BLOCK_SIZE] = {0};ret = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_ENCRYPT, padded_len, iv,padded, ciphertext);mbedtls_aes_free(&aes_ctx);delete[] padded;if (ret != 0) {log_error("AES encryption failed: %d", ret);return false;}cipher_len = padded_len;return true;}bool CredentialManager::decryptPassword(const uint8_t *ciphertext, uint16_t cipher_len, char *plaintext, uint16_t max_len) {if (!ciphertext || !plaintext || cipher_len == 0 ||cipher_len % AES_BLOCK_SIZE != 0) {log_error("Invalid decryption parameters");return false;}if (cipher_len > max_len) {log_error("Buffer too small for decryption");return false;}// AES-256-CBC decryptionmbedtls_aes_context aes_ctx;mbedtls_aes_init(&aes_ctx);int ret = mbedtls_aes_setkey_dec(&aes_ctx, m_encryption_key, 256);if (ret != 0) {log_error("AES setkey failed: %d", ret);mbedtls_aes_free(&aes_ctx);return false;}uint8_t iv[AES_BLOCK_SIZE] = {0};uint8_t *decrypted = new uint8_t[cipher_len];ret = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, cipher_len, iv,ciphertext, decrypted);mbedtls_aes_free(&aes_ctx);if (ret != 0) {log_error("AES decryption failed: %d", ret);delete[] decrypted;return false;}// Remove PKCS7 paddinguint16_t plaintext_len = removePKCS7Padding(decrypted, cipher_len);if (plaintext_len == 0) {log_error("Invalid padding in decrypted data");delete[] decrypted;return false;}if (plaintext_len >= max_len) {log_error("Plaintext too long");delete[] decrypted;return false;}memcpy(plaintext, decrypted, plaintext_len);plaintext[plaintext_len] = '\0';// Clean sensitive data from memorymemset(decrypted, 0, cipher_len);delete[] decrypted;return true;}void CredentialManager::applyPKCS7Padding(const uint8_t *data, uint16_t data_len, uint8_t *padded, uint16_t &padded_len) {// Calculate length with padding (multiple of 16)uint16_t total_len = ((data_len / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE;uint8_t pad_value = total_len - data_len;memcpy(padded, data, data_len);for (int i = data_len; i < total_len; i++) {padded[i] = pad_value;}padded_len = total_len;}uint16_t CredentialManager::removePKCS7Padding(uint8_t *data, uint16_t data_len) {if (data_len < AES_BLOCK_SIZE || data_len % AES_BLOCK_SIZE != 0) {return 0;}uint8_t pad_value = data[data_len - 1];// Validate paddingif (pad_value > AES_BLOCK_SIZE || pad_value == 0) {return 0;}// Verify that all padding bytes have the correct valuefor (int i = data_len - pad_value; i < data_len; i++) {if (data[i] != pad_value) {return 0;}}return data_len - pad_value;}#if defined(ESP8266)// Attach a filesystem instance used for persisting credentials on ESP8266.// This must be called before saveToFS()/loadFromFS().void CredentialManager::setFilesystem(fs::FS* fs) {m_filesystem = fs;}// Save credentials to FS in a simple binary format:// [uint8_t count][char hostname[33]] then for each credential:// [char ssid[33]][uint16_t pwd_len][uint8_t pwd_encrypted[64]]// [uint32_t gateway][uint32_t subnet][uint32_t local_ip]// [uint32_t dns1][uint32_t dns2]bool CredentialManager::saveToFS(const char* filepath) {if (!m_filesystem) {log_error("Filesystem not set");return false;}File file = m_filesystem->open(filepath, "w");if (!file) {log_error("Failed to open %s for write", filepath);return false;}uint8_t count = m_credentials.size();if (count > MAX_CREDENTIALS) {count = MAX_CREDENTIALS;}// New simple format (breaking change is acceptable):// [count][hostname][credentials...]file.write(&count, 1);file.write((const uint8_t*)m_hostname, sizeof(m_hostname));for (uint8_t i = 0; i < count; i++) {const WiFiCredential &cred = m_credentials[i];file.write((const uint8_t*)cred.ssid, sizeof(cred.ssid));file.write((const uint8_t*)&cred.pwd_len, sizeof(cred.pwd_len));file.write((const uint8_t*)cred.pwd_encrypted, sizeof(cred.pwd_encrypted));uint32_t gw_addr = cred.gateway;uint32_t sn_addr = cred.subnet;uint32_t ip_addr = cred.local_ip;uint32_t dns1_addr_cred = cred.dns1;uint32_t dns2_addr_cred = cred.dns2;file.write((const uint8_t*)&gw_addr, sizeof(gw_addr));file.write((const uint8_t*)&sn_addr, sizeof(sn_addr));file.write((const uint8_t*)&ip_addr, sizeof(ip_addr));file.write((const uint8_t*)&dns1_addr_cred, sizeof(dns1_addr_cred));file.write((const uint8_t*)&dns2_addr_cred, sizeof(dns2_addr_cred));}file.close();log_info("Credentials saved to FS (%d entries)", count);return true;}// Load credentials from FS written by saveToFS().// Uses the same binary layout as described above.bool CredentialManager::loadFromFS(const char* filepath) {if (!m_filesystem) {log_error("Filesystem not set");return false;}if (!m_filesystem->exists(filepath)) {log_info("No credentials file found: %s", filepath);return false;}File file = m_filesystem->open(filepath, "r");if (!file) {log_error("Failed to open %s for read", filepath);return false;}m_credentials.clear();uint8_t count = 0;if (file.read(&count, 1) != 1 || count == 0) {file.close();log_debug("No credentials in FS");return false;}if (count > MAX_CREDENTIALS) {count = MAX_CREDENTIALS;}// Simple new format: hostname directly after countif (file.read((uint8_t*)m_hostname, sizeof(m_hostname)) != sizeof(m_hostname)) {memset(m_hostname, 0, sizeof(m_hostname));}for (uint8_t i = 0; i < count; i++) {WiFiCredential cred{};if (file.read((uint8_t*)cred.ssid, sizeof(cred.ssid)) != sizeof(cred.ssid)) break;if (file.read((uint8_t*)&cred.pwd_len, sizeof(cred.pwd_len)) != sizeof(cred.pwd_len)) break;if (cred.pwd_len == 0 || cred.pwd_len > sizeof(cred.pwd_encrypted)) break;if (file.read((uint8_t*)cred.pwd_encrypted, sizeof(cred.pwd_encrypted)) != sizeof(cred.pwd_encrypted)) break;uint32_t gw_addr = 0;uint32_t sn_addr = 0;uint32_t ip_addr = 0;if (file.read((uint8_t*)&gw_addr, sizeof(gw_addr)) != sizeof(gw_addr)) break;if (file.read((uint8_t*)&sn_addr, sizeof(sn_addr)) != sizeof(sn_addr)) break;if (file.read((uint8_t*)&ip_addr, sizeof(ip_addr)) != sizeof(ip_addr)) break;cred.gateway = IPAddress(gw_addr);cred.subnet = IPAddress(sn_addr);cred.local_ip = IPAddress(ip_addr);// Optional per-credential DNS (backward compatible)uint32_t dns1_addr_cred = 0;uint32_t dns2_addr_cred = 0;if ((unsigned int)file.available() >= sizeof(dns1_addr_cred) + sizeof(dns2_addr_cred)) {if (file.read((uint8_t*)&dns1_addr_cred, sizeof(dns1_addr_cred)) == sizeof(dns1_addr_cred) &&file.read((uint8_t*)&dns2_addr_cred, sizeof(dns2_addr_cred)) == sizeof(dns2_addr_cred)) {cred.dns1 = IPAddress(dns1_addr_cred);cred.dns2 = IPAddress(dns2_addr_cred);}} else {cred.dns1 = IPAddress(0, 0, 0, 0);cred.dns2 = IPAddress(0, 0, 0, 0);}m_credentials.push_back(cred);}file.close();if (m_credentials.size() > 0) {log_info("Loaded %d credentials from FS", m_credentials.size());return true;}return false;}#endif#if defined(ESP32)// Save credentials to ESP32 NVS in a key/value layout.// Global:// host -> hostname (null-terminated string)// count -> number of stored credentials (uint8_t)// Per credential i (0..count-1):// ssid{i} -> SSID string// pass{i} -> encrypted password blob (length len{i})// len{i} -> encrypted password length (uint16_t)// gw{i} -> gateway IPv4 as uint32_t// sn{i} -> subnet mask IPv4 as uint32_t// ip{i} -> local IP IPv4 as uint32_t// dns1_{i} -> primary DNS IPv4 as uint32_t// dns2_{i} -> secondary DNS IPv4 as uint32_tbool CredentialManager::saveToNVS(const char *nvs_namespace) {nvs_handle_t nvs_handle;esp_err_t err = nvs_open(nvs_namespace, NVS_READWRITE, &nvs_handle);if (err != ESP_OK) {log_error("Failed to open NVS: %s", esp_err_to_name(err));return false;}// Save global hostname (no DNS/port globals anymore)err = nvs_set_str(nvs_handle, "host", m_hostname);if (err != ESP_OK) {log_error("Failed to save hostname");nvs_close(nvs_handle);return false;}// Save number of credentialserr = nvs_set_u8(nvs_handle, "count", m_credentials.size());if (err != ESP_OK) {log_error("Failed to save credentials count");nvs_close(nvs_handle);return false;}// Save each credentialfor (size_t i = 0; i < m_credentials.size(); i++) {const WiFiCredential &cred = m_credentials[i];// SSID keyString key_ssid = "ssid" + String(i);err = nvs_set_str(nvs_handle, key_ssid.c_str(), cred.ssid);if (err != ESP_OK) {log_error("Failed to save SSID %d", i);nvs_close(nvs_handle);return false;}// Encrypted password key (as blob)String key_pass = "pass" + String(i);err = nvs_set_blob(nvs_handle, key_pass.c_str(), cred.pwd_encrypted, cred.pwd_len);if (err != ESP_OK) {log_error("Failed to save password %d", i);nvs_close(nvs_handle);return false;}// Password lengthString key_len = "len" + String(i);err = nvs_set_u16(nvs_handle, key_len.c_str(), cred.pwd_len);if (err != ESP_OK) {log_error("Failed to save password length %d", i);nvs_close(nvs_handle);return false;}// Static IP Configuration - GatewayString key_gw = "gw" + String(i);uint32_t gw_addr = cred.gateway;err = nvs_set_u32(nvs_handle, key_gw.c_str(), gw_addr);if (err != ESP_OK) {log_error("Failed to save gateway %d", i);nvs_close(nvs_handle);return false;}// Static IP Configuration - SubnetString key_sn = "sn" + String(i);uint32_t sn_addr = cred.subnet;err = nvs_set_u32(nvs_handle, key_sn.c_str(), sn_addr);if (err != ESP_OK) {log_error("Failed to save subnet %d", i);nvs_close(nvs_handle);return false;}// Static IP Configuration - Local IPString key_ip = "ip" + String(i);uint32_t ip_addr = cred.local_ip;err = nvs_set_u32(nvs_handle, key_ip.c_str(), ip_addr);if (err != ESP_OK) {log_error("Failed to save local IP %d", i);nvs_close(nvs_handle);return false;}// Static DNS configuration (per credential, optional)String key_dns1 = "dns1_" + String(i);uint32_t dns1_addr_cred = cred.dns1;err = nvs_set_u32(nvs_handle, key_dns1.c_str(), dns1_addr_cred);if (err != ESP_OK) {log_error("Failed to save DNS1 %d", i);nvs_close(nvs_handle);return false;}String key_dns2 = "dns2_" + String(i);uint32_t dns2_addr_cred = cred.dns2;err = nvs_set_u32(nvs_handle, key_dns2.c_str(), dns2_addr_cred);if (err != ESP_OK) {log_error("Failed to save DNS2 %d", i);nvs_close(nvs_handle);return false;}}err = nvs_commit(nvs_handle);nvs_close(nvs_handle);if (err == ESP_OK) {log_info("Credentials saved to NVS (%d entries)", m_credentials.size());return true;} else {log_error("Failed to commit NVS");return false;}}// Load credentials from ESP32 NVS using the layout documented// in saveToNVS(). If any entry is missing, sensible defaults// (e.g. 0.0.0.0 for IP/DNS) are applied.bool CredentialManager::loadFromNVS(const char *nvs_namespace) {m_credentials.clear();nvs_handle_t nvs_handle;esp_err_t err = nvs_open(nvs_namespace, NVS_READONLY, &nvs_handle);if (err != ESP_OK) {log_info("No NVS data found for namespace: %s", nvs_namespace);return false;}size_t host_len = sizeof(m_hostname);if (nvs_get_str(nvs_handle, "host", m_hostname, &host_len) != ESP_OK) {memset(m_hostname, 0, sizeof(m_hostname));}// Read number of credentialsuint8_t count = 0;err = nvs_get_u8(nvs_handle, "count", &count);if (err != ESP_OK || count == 0) {log_debug("No credentials in NVS");nvs_close(nvs_handle);return false;}if (count > MAX_CREDENTIALS) {count = MAX_CREDENTIALS;}// Read each credentialfor (int i = 0; i < count; i++) {WiFiCredential cred;memset(&cred, 0, sizeof(cred));// Read SSIDString key_ssid = "ssid" + String(i);size_t ssid_len = 33;err = nvs_get_str(nvs_handle, key_ssid.c_str(), cred.ssid, &ssid_len);if (err != ESP_OK) {log_error("Failed to load SSID %d", i);continue;}// Read password lengthString key_len = "len" + String(i);uint16_t pass_len = 0;err = nvs_get_u16(nvs_handle, key_len.c_str(), &pass_len);if (err != ESP_OK || pass_len == 0 || pass_len > 64) {log_error("Invalid password length for credential %d", i);continue;}// Read encrypted passwordString key_pass = "pass" + String(i);size_t blob_len = pass_len;err = nvs_get_blob(nvs_handle, key_pass.c_str(), cred.pwd_encrypted, &blob_len);if (err != ESP_OK) {log_error("Failed to load password %d", i);continue;}cred.pwd_len = pass_len;// Read Static IP Configuration - GatewayString key_gw = "gw" + String(i);uint32_t gw_addr = 0;err = nvs_get_u32(nvs_handle, key_gw.c_str(), &gw_addr);if (err == ESP_OK) {cred.gateway = IPAddress(gw_addr);} else {cred.gateway = IPAddress(0, 0, 0, 0);}// Read Static IP Configuration - SubnetString key_sn = "sn" + String(i);uint32_t sn_addr = 0;err = nvs_get_u32(nvs_handle, key_sn.c_str(), &sn_addr);if (err == ESP_OK) {cred.subnet = IPAddress(sn_addr);} else {cred.subnet = IPAddress(0, 0, 0, 0);}// Read Static IP Configuration - Local IPString key_ip = "ip" + String(i);uint32_t ip_addr = 0;err = nvs_get_u32(nvs_handle, key_ip.c_str(), &ip_addr);if (err == ESP_OK) {cred.local_ip = IPAddress(ip_addr);} else {cred.local_ip = IPAddress(0, 0, 0, 0);}// Read Static DNS configuration (per credential, optional)String key_dns1 = "dns1_" + String(i);uint32_t dns1_addr_cred = 0;if (nvs_get_u32(nvs_handle, key_dns1.c_str(), &dns1_addr_cred) == ESP_OK) {cred.dns1 = IPAddress(dns1_addr_cred);} else {cred.dns1 = IPAddress(0, 0, 0, 0);}String key_dns2 = "dns2_" + String(i);uint32_t dns2_addr_cred = 0;if (nvs_get_u32(nvs_handle, key_dns2.c_str(), &dns2_addr_cred) == ESP_OK) {cred.dns2 = IPAddress(dns2_addr_cred);} else {cred.dns2 = IPAddress(0, 0, 0, 0);}m_credentials.push_back(cred);}nvs_close(nvs_handle);if (m_credentials.size() > 0) {log_info("Loaded %d credentials from NVS", m_credentials.size());return true;}return false;}#endifString CredentialManager::getDebugInfo() const {String info = "\n=== Credential Manager Debug Info ===\n";info += "Encryption Status: " + getStatus() + "\n";info += "Credentials Count: " + String(m_credentials.size()) + "/" +String(MAX_CREDENTIALS) + "\n";#if defined(ESP32)info += "BLOCK_KEY0 Status: ";// Read BLOCK_KEY0 statusuint8_t efuse_key[32];esp_err_t err = esp_efuse_read_block(EFUSE_BLK_KEY0, efuse_key, 0, 256);if (err == ESP_OK) {bool all_ff = true;bool all_00 = true;for (int i = 0; i < 32; i++) {if (efuse_key[i] != 0xFF)all_ff = false;if (efuse_key[i] != 0x00)all_00 = false;}if (all_ff) {info += "NOT PROGRAMMED (all 0xFF)\n";} else if (all_00) {info += "INVALID (all 0x00)\n";} else {info += "PROGRAMMED (bytes present)\n";}} else {info += "ERROR: " + String(esp_err_to_name(err)) + "\n";}#elseinfo += "BLOCK_KEY0 Status: N/A (not ESP32)\n";#endifinfo += "\nCredentials:\n";for (size_t i = 0; i < m_credentials.size(); i++) {info += " [" + String(i) + "] SSID: " + String(m_credentials[i].ssid);// Show IP configuration if not all zerosif (m_credentials[i].local_ip != IPAddress(0, 0, 0, 0)) {info += " | IP: " + m_credentials[i].local_ip.toString();info += " | GW: " + m_credentials[i].gateway.toString();info += " | SN: " + m_credentials[i].subnet.toString();} else {info += " | IP: DHCP";}info += " | Encrypted len: " + String(m_credentials[i].pwd_len) + "\n";}info += "=====================================\n";return info;}