Subversion Repositories ESP8266_P1_Meter

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 raymond 1
#include "CredentialManager.h"
2
#include "SerialLog.h"
3
 
4
 
5
CredentialManager::CredentialManager() : m_efuse_initialized(false) {
6
  memset(m_encryption_key, 0, ENCRYPTION_KEY_SIZE);
7
 
8
  // Default global options (hostname only)
9
  memset(m_hostname, 0, sizeof(m_hostname));
10
 
11
#if defined(ESP8266)
12
  m_filesystem = nullptr;
13
#endif
14
}
15
 
16
CredentialManager::~CredentialManager() {
17
  // Clean key from memory
18
  memset(m_encryption_key, 0, ENCRYPTION_KEY_SIZE);
19
  clearAll();
20
}
21
 
22
bool CredentialManager::begin() {
23
  initializeEncryptionKey();
24
  return true;
25
}
26
 
27
void CredentialManager::initializeEncryptionKey() {
28
#if defined(ESP32)
29
  esp_err_t err = esp_efuse_read_block(EFUSE_BLK_KEY0, m_encryption_key, 0,
30
                                       ENCRYPTION_KEY_SIZE * 8);
31
 
32
  if (err == ESP_OK) {
33
    // Check if truly programmed (not all 0xFF)
34
    bool all_ff = true;
35
    for (int i = 0; i < ENCRYPTION_KEY_SIZE; i++) {
36
      if (m_encryption_key[i] != 0xFF) {
37
        all_ff = false;
38
        break;
39
      }
40
    }
41
 
42
    if (!all_ff) {
43
      m_efuse_initialized = true;
44
      log_info("BLOCK_KEY0 initialized from eFuse - SECURE mode");
45
    } else {
46
      log_info("BLOCK_KEY0 not programmed - using fallback (NOT SECURE)");
47
      m_efuse_initialized = false;
48
      // Use fallback key (derived from MAC address or constant)
49
      // This obfuscates but is not real security
50
      memset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);
51
    }
52
  } else {
53
    log_error("Failed to read BLOCK_KEY0: %s", esp_err_to_name(err));
54
    // Fallback to predefined key
55
    memset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);
56
  }
57
#else
58
  // For ESP8266 or other chips, use fallback
59
  memset(m_encryption_key, 0xAA, ENCRYPTION_KEY_SIZE);
60
#endif
61
}
62
 
63
String CredentialManager::getStatus() const {
64
  if (m_efuse_initialized) {
65
    return "SECURE (BLOCK_KEY0 programmed)";
66
  } else {
67
    return "INSECURE (BLOCK_KEY0 not programmed - fallback key used)";
68
  }
69
}
70
 
71
void CredentialManager::setHostname(const char* hostname) {
72
  if (!hostname) {
73
    memset(m_hostname, 0, sizeof(m_hostname));
74
    return;
75
  }
76
  strlcpy(m_hostname, hostname, sizeof(m_hostname));
77
}
78
 
79
String CredentialManager::getHostname() const {
80
  return String(m_hostname);
81
}
82
 
83
// Web server port is fixed at construction time in AsyncFsWebServer
84
// and is not stored as a shared, mutable option in CredentialManager.
85
 
86
bool CredentialManager::addCredential(const WiFiCredential& credential, const char* plaintext_password) {
87
  if (strlen(credential.ssid) == 0) {
88
    log_error("Invalid SSID");
89
    return false;
90
  }
91
  if (!plaintext_password || strlen(plaintext_password) == 0) {
92
    log_error("Invalid password");
93
    return false;
94
  }
95
 
96
  if (m_credentials.size() >= MAX_CREDENTIALS) {
97
    log_error("Maximum credentials reached (%d)", MAX_CREDENTIALS);
98
    return false;
99
  }
100
 
101
  if (strlen(plaintext_password) > 63) {
102
    log_error("Password too long (max 63 characters)");
103
    return false;
104
  }
105
 
106
  // Create credential copy with encrypted password
107
  WiFiCredential cred = credential;
108
 
109
  // Encrypt password
110
  uint16_t encrypted_len = 0;
111
  if (!encryptPassword(plaintext_password, cred.pwd_encrypted, encrypted_len)) {
112
    log_error("Failed to encrypt password");
113
    return false;
114
  }
115
 
116
  cred.pwd_len = encrypted_len;
117
 
118
  m_credentials.push_back(cred);
119
  log_debug("Credential added: %s.", cred.ssid);
120
 
121
  return true;
122
}
123
 
124
bool CredentialManager::removeCredential(uint8_t index) {
125
  if (index >= m_credentials.size()) {
126
    log_error("Invalid credential index %d", index);
127
    return false;
128
  }
129
 
130
  log_debug("Removing credential: %s", m_credentials[index].ssid);
131
  memset(m_credentials[index].pwd_encrypted, 0, sizeof(m_credentials[index].pwd_encrypted));
132
  memset(m_credentials[index].ssid, 0, sizeof(m_credentials[index].ssid));
133
  m_credentials.erase(m_credentials.begin() + index);
134
  return true;
135
}
136
 
137
bool CredentialManager::removeCredential(const char* ssid) {
138
  if (!ssid) {
139
    return false;
140
  }
141
 
142
  for (size_t i = 0; i < m_credentials.size(); i++) {
143
    if (strcmp(m_credentials[i].ssid, ssid) == 0) {
144
      return removeCredential(static_cast<uint8_t>(i));
145
    }
146
  }
147
 
148
  log_error("Credential not found: %s", ssid);
149
  return false;
150
}
151
 
152
bool CredentialManager::updateCredential(const WiFiCredential& credential, const char* plaintext_password) {
153
  uint8_t index = 255;
154
  for (size_t i = 0; i < m_credentials.size(); i++) {
155
    if (strcmp(m_credentials[i].ssid, credential.ssid) == 0) {
156
      index = i;
157
      break;
158
    }
159
  }
160
 
161
  if (index >= m_credentials.size()) {
162
    log_error("Credential not found: %s", credential.ssid);
163
    return false;
164
  }
165
 
166
  if (!plaintext_password || strlen(plaintext_password) == 0) {
167
    log_error("Invalid password");
168
    return false;
169
  }
170
 
171
  if (strlen(plaintext_password) > 63) {
172
    log_error("Password too long (max 63 characters)");
173
    return false;
174
  }
175
 
176
  WiFiCredential &cred = m_credentials[index];
177
 
178
  // Encrypt new password
179
  uint16_t encrypted_len = 0;
180
  if (!encryptPassword(plaintext_password, cred.pwd_encrypted, encrypted_len)) {
181
    log_error("Failed to encrypt password");
182
    return false;
183
  }
184
 
185
  cred.pwd_len = encrypted_len;
186
 
187
  // Update IP configuration from credential parameter
188
  cred.gateway = credential.gateway;
189
  cred.subnet = credential.subnet;
190
  cred.local_ip = credential.local_ip;
191
  cred.dns1 = credential.dns1;
192
  cred.dns2 = credential.dns2;
193
 
194
  log_debug("Credential updated: %s.", credential.ssid);
195
  return true;
196
}
197
 
198
const char *CredentialManager::getSSID(uint8_t index) const {
199
  if (index >= m_credentials.size()) {
200
    return nullptr;
201
  }
202
  return m_credentials[index].ssid;
203
}
204
 
205
String CredentialManager::getPassword(uint8_t index) {
206
  if (index >= m_credentials.size()) {
207
    return "";
208
  }
209
 
210
  const WiFiCredential &cred = m_credentials[index];
211
  char plaintext[65] = {0};
212
 
213
  if (decryptPassword(cred.pwd_encrypted, cred.pwd_len, plaintext,
214
                      64)) {
215
    String result = plaintext;
216
    // Clean password from memory
217
    memset(plaintext, 0, 65);
218
    return result;
219
  }
220
 
221
  return "";
222
}
223
 
224
bool CredentialManager::setIPConfiguration(uint8_t index, IPAddress ip, IPAddress gateway, IPAddress subnet) {
225
  if (index >= m_credentials.size()) {
226
    log_error("Invalid credential index %d", index);
227
    return false;
228
  }
229
 
230
  m_credentials[index].local_ip = ip;
231
  m_credentials[index].gateway = gateway;
232
  m_credentials[index].subnet = subnet;
233
 
234
  log_debug("IP configuration updated for credential %d: IP=%s, GW=%s, SN=%s", 
235
            index, ip.toString().c_str(), gateway.toString().c_str(), subnet.toString().c_str());
236
  return true;
237
}
238
 
239
bool CredentialManager::getIPConfiguration(uint8_t index, IPAddress& ip, IPAddress& gateway, IPAddress& subnet) const {
240
  if (index >= m_credentials.size()) {
241
    log_error("Invalid credential index %d", index);
242
    return false;
243
  }
244
 
245
  ip = m_credentials[index].local_ip;
246
  gateway = m_credentials[index].gateway;
247
  subnet = m_credentials[index].subnet;
248
 
249
  return true;
250
}
251
 
252
void CredentialManager::clearAll() {
253
  // Clean each credential
254
  for (auto &cred : m_credentials) {
255
    memset(cred.pwd_encrypted, 0, sizeof(cred.pwd_encrypted));
256
    memset(cred.ssid, 0, sizeof(cred.ssid));
257
  }
258
  m_credentials.clear();
259
  #if defined(ESP8266)
260
    saveToFS();
261
  #elif defined(ESP32)
262
    saveToNVS();
263
  #endif
264
  log_debug("All credentials cleared");
265
}
266
 
267
bool CredentialManager::encryptPassword(const char *plaintext, uint8_t *ciphertext, uint16_t &cipher_len) {
268
  if (!plaintext || !ciphertext) {
269
    return false;
270
  }
271
 
272
  size_t plaintext_len = strlen(plaintext);
273
  if (plaintext_len > 63) {
274
    log_error("Password too long");
275
    return false;
276
  }
277
 
278
  // Apply PKCS7 padding
279
  uint8_t *padded = new uint8_t[64];
280
  uint16_t padded_len = 0;
281
  applyPKCS7Padding((uint8_t *)plaintext, plaintext_len, padded, padded_len);
282
 
283
  // AES-256-CBC encryption
284
  mbedtls_aes_context aes_ctx;
285
  mbedtls_aes_init(&aes_ctx);
286
 
287
  int ret = mbedtls_aes_setkey_enc(&aes_ctx, m_encryption_key, 256);
288
  if (ret != 0) {
289
    log_error("AES setkey failed: %d", ret);
290
    mbedtls_aes_free(&aes_ctx);
291
    delete[] padded;
292
    return false;
293
  }
294
 
295
  // Fixed IV (deterministic, not randomized)
296
  // In case of randomized IV, save it in the first block
297
  uint8_t iv[AES_BLOCK_SIZE] = {0};
298
 
299
  ret = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_ENCRYPT, padded_len, iv,
300
                              padded, ciphertext);
301
 
302
  mbedtls_aes_free(&aes_ctx);
303
  delete[] padded;
304
 
305
  if (ret != 0) {
306
    log_error("AES encryption failed: %d", ret);
307
    return false;
308
  }
309
 
310
  cipher_len = padded_len;
311
  return true;
312
}
313
 
314
bool CredentialManager::decryptPassword(const uint8_t *ciphertext, uint16_t cipher_len, char *plaintext, uint16_t max_len) {
315
  if (!ciphertext || !plaintext || cipher_len == 0 ||
316
      cipher_len % AES_BLOCK_SIZE != 0) {
317
    log_error("Invalid decryption parameters");
318
    return false;
319
  }
320
 
321
  if (cipher_len > max_len) {
322
    log_error("Buffer too small for decryption");
323
    return false;
324
  }
325
 
326
  // AES-256-CBC decryption
327
  mbedtls_aes_context aes_ctx;
328
  mbedtls_aes_init(&aes_ctx);
329
 
330
  int ret = mbedtls_aes_setkey_dec(&aes_ctx, m_encryption_key, 256);
331
  if (ret != 0) {
332
    log_error("AES setkey failed: %d", ret);
333
    mbedtls_aes_free(&aes_ctx);
334
    return false;
335
  }
336
 
337
  uint8_t iv[AES_BLOCK_SIZE] = {0};
338
  uint8_t *decrypted = new uint8_t[cipher_len];
339
 
340
  ret = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, cipher_len, iv,
341
                              ciphertext, decrypted);
342
 
343
  mbedtls_aes_free(&aes_ctx);
344
 
345
  if (ret != 0) {
346
    log_error("AES decryption failed: %d", ret);
347
    delete[] decrypted;
348
    return false;
349
  }
350
 
351
  // Remove PKCS7 padding
352
  uint16_t plaintext_len = removePKCS7Padding(decrypted, cipher_len);
353
  if (plaintext_len == 0) {
354
    log_error("Invalid padding in decrypted data");
355
    delete[] decrypted;
356
    return false;
357
  }
358
 
359
  if (plaintext_len >= max_len) {
360
    log_error("Plaintext too long");
361
    delete[] decrypted;
362
    return false;
363
  }
364
 
365
  memcpy(plaintext, decrypted, plaintext_len);
366
  plaintext[plaintext_len] = '\0';
367
 
368
  // Clean sensitive data from memory
369
  memset(decrypted, 0, cipher_len);
370
  delete[] decrypted;
371
 
372
  return true;
373
}
374
 
375
void CredentialManager::applyPKCS7Padding(const uint8_t *data, uint16_t data_len, uint8_t *padded, uint16_t &padded_len) {
376
  // Calculate length with padding (multiple of 16)
377
  uint16_t total_len = ((data_len / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE;
378
  uint8_t pad_value = total_len - data_len;
379
 
380
  memcpy(padded, data, data_len);
381
  for (int i = data_len; i < total_len; i++) {
382
    padded[i] = pad_value;
383
  }
384
 
385
  padded_len = total_len;
386
}
387
 
388
uint16_t CredentialManager::removePKCS7Padding(uint8_t *data, uint16_t data_len) {
389
  if (data_len < AES_BLOCK_SIZE || data_len % AES_BLOCK_SIZE != 0) {
390
    return 0;
391
  }
392
 
393
  uint8_t pad_value = data[data_len - 1];
394
 
395
  // Validate padding
396
  if (pad_value > AES_BLOCK_SIZE || pad_value == 0) {
397
    return 0;
398
  }
399
 
400
  // Verify that all padding bytes have the correct value
401
  for (int i = data_len - pad_value; i < data_len; i++) {
402
    if (data[i] != pad_value) {
403
      return 0;
404
    }
405
  }
406
 
407
  return data_len - pad_value;
408
}
409
 
410
#if defined(ESP8266)
411
// Attach a filesystem instance used for persisting credentials on ESP8266.
412
// This must be called before saveToFS()/loadFromFS().
413
void CredentialManager::setFilesystem(fs::FS* fs) {
414
  m_filesystem = fs;
415
}
416
 
417
// Save credentials to FS in a simple binary format:
418
// [uint8_t count][char hostname[33]] then for each credential:
419
// [char ssid[33]][uint16_t pwd_len][uint8_t pwd_encrypted[64]]
420
// [uint32_t gateway][uint32_t subnet][uint32_t local_ip]
421
// [uint32_t dns1][uint32_t dns2]
422
bool CredentialManager::saveToFS(const char* filepath) {
423
  if (!m_filesystem) {
424
    log_error("Filesystem not set");
425
    return false;
426
  }
427
 
428
  File file = m_filesystem->open(filepath, "w");
429
  if (!file) {
430
    log_error("Failed to open %s for write", filepath);
431
    return false;
432
  }
433
 
434
  uint8_t count = m_credentials.size();
435
  if (count > MAX_CREDENTIALS) {
436
    count = MAX_CREDENTIALS;
437
  }
438
 
439
  // New simple format (breaking change is acceptable):
440
  // [count][hostname][credentials...]
441
  file.write(&count, 1);
442
  file.write((const uint8_t*)m_hostname, sizeof(m_hostname));
443
 
444
  for (uint8_t i = 0; i < count; i++) {
445
    const WiFiCredential &cred = m_credentials[i];
446
 
447
    file.write((const uint8_t*)cred.ssid, sizeof(cred.ssid));
448
    file.write((const uint8_t*)&cred.pwd_len, sizeof(cred.pwd_len));
449
    file.write((const uint8_t*)cred.pwd_encrypted, sizeof(cred.pwd_encrypted));
450
 
451
    uint32_t gw_addr = cred.gateway;
452
    uint32_t sn_addr = cred.subnet;
453
    uint32_t ip_addr = cred.local_ip;
454
    uint32_t dns1_addr_cred = cred.dns1;
455
    uint32_t dns2_addr_cred = cred.dns2;
456
    file.write((const uint8_t*)&gw_addr, sizeof(gw_addr));
457
    file.write((const uint8_t*)&sn_addr, sizeof(sn_addr));
458
    file.write((const uint8_t*)&ip_addr, sizeof(ip_addr));
459
    file.write((const uint8_t*)&dns1_addr_cred, sizeof(dns1_addr_cred));
460
    file.write((const uint8_t*)&dns2_addr_cred, sizeof(dns2_addr_cred));
461
  }
462
 
463
  file.close();
464
  log_info("Credentials saved to FS (%d entries)", count);
465
  return true;
466
}
467
 
468
// Load credentials from FS written by saveToFS().
469
// Uses the same binary layout as described above.
470
bool CredentialManager::loadFromFS(const char* filepath) {
471
  if (!m_filesystem) {
472
    log_error("Filesystem not set");
473
    return false;
474
  }
475
 
476
  if (!m_filesystem->exists(filepath)) {
477
    log_info("No credentials file found: %s", filepath);
478
    return false;
479
  }
480
 
481
  File file = m_filesystem->open(filepath, "r");
482
  if (!file) {
483
    log_error("Failed to open %s for read", filepath);
484
    return false;
485
  }
486
 
487
  m_credentials.clear();
488
 
489
  uint8_t count = 0;
490
  if (file.read(&count, 1) != 1 || count == 0) {
491
    file.close();
492
    log_debug("No credentials in FS");
493
    return false;
494
  }
495
 
496
  if (count > MAX_CREDENTIALS) {
497
    count = MAX_CREDENTIALS;
498
  }
499
 
500
  // Simple new format: hostname directly after count
501
  if (file.read((uint8_t*)m_hostname, sizeof(m_hostname)) != sizeof(m_hostname)) {
502
    memset(m_hostname, 0, sizeof(m_hostname));
503
  }
504
 
505
  for (uint8_t i = 0; i < count; i++) {
506
    WiFiCredential cred{};
507
 
508
    if (file.read((uint8_t*)cred.ssid, sizeof(cred.ssid)) != sizeof(cred.ssid)) break;
509
    if (file.read((uint8_t*)&cred.pwd_len, sizeof(cred.pwd_len)) != sizeof(cred.pwd_len)) break;
510
    if (cred.pwd_len == 0 || cred.pwd_len > sizeof(cred.pwd_encrypted)) break;
511
    if (file.read((uint8_t*)cred.pwd_encrypted, sizeof(cred.pwd_encrypted)) != sizeof(cred.pwd_encrypted)) break;
512
 
513
    uint32_t gw_addr = 0;
514
    uint32_t sn_addr = 0;
515
    uint32_t ip_addr = 0;
516
    if (file.read((uint8_t*)&gw_addr, sizeof(gw_addr)) != sizeof(gw_addr)) break;
517
    if (file.read((uint8_t*)&sn_addr, sizeof(sn_addr)) != sizeof(sn_addr)) break;
518
    if (file.read((uint8_t*)&ip_addr, sizeof(ip_addr)) != sizeof(ip_addr)) break;
519
 
520
    cred.gateway = IPAddress(gw_addr);
521
    cred.subnet = IPAddress(sn_addr);
522
    cred.local_ip = IPAddress(ip_addr);
523
 
524
    // Optional per-credential DNS (backward compatible)
525
    uint32_t dns1_addr_cred = 0;
526
    uint32_t dns2_addr_cred = 0;
527
    if ((unsigned int)file.available() >= sizeof(dns1_addr_cred) + sizeof(dns2_addr_cred)) {
528
      if (file.read((uint8_t*)&dns1_addr_cred, sizeof(dns1_addr_cred)) == sizeof(dns1_addr_cred) &&
529
          file.read((uint8_t*)&dns2_addr_cred, sizeof(dns2_addr_cred)) == sizeof(dns2_addr_cred)) {
530
        cred.dns1 = IPAddress(dns1_addr_cred);
531
        cred.dns2 = IPAddress(dns2_addr_cred);
532
      }
533
    } else {
534
      cred.dns1 = IPAddress(0, 0, 0, 0);
535
      cred.dns2 = IPAddress(0, 0, 0, 0);
536
    }
537
 
538
    m_credentials.push_back(cred);
539
  }
540
 
541
  file.close();
542
 
543
  if (m_credentials.size() > 0) {
544
    log_info("Loaded %d credentials from FS", m_credentials.size());
545
    return true;
546
  }
547
 
548
  return false;
549
}
550
#endif
551
 
552
 
553
#if defined(ESP32)
554
// Save credentials to ESP32 NVS in a key/value layout.
555
// Global:
556
//   host  -> hostname (null-terminated string)
557
//   count -> number of stored credentials (uint8_t)
558
// Per credential i (0..count-1):
559
//   ssid{i}   -> SSID string
560
//   pass{i}   -> encrypted password blob (length len{i})
561
//   len{i}    -> encrypted password length (uint16_t)
562
//   gw{i}     -> gateway IPv4 as uint32_t
563
//   sn{i}     -> subnet mask IPv4 as uint32_t
564
//   ip{i}     -> local IP IPv4 as uint32_t
565
//   dns1_{i}  -> primary DNS IPv4 as uint32_t
566
//   dns2_{i}  -> secondary DNS IPv4 as uint32_t
567
bool CredentialManager::saveToNVS(const char *nvs_namespace) {
568
 
569
  nvs_handle_t nvs_handle;
570
  esp_err_t err = nvs_open(nvs_namespace, NVS_READWRITE, &nvs_handle);
571
 
572
  if (err != ESP_OK) {
573
    log_error("Failed to open NVS: %s", esp_err_to_name(err));
574
    return false;
575
  }
576
 
577
  // Save global hostname (no DNS/port globals anymore)
578
  err = nvs_set_str(nvs_handle, "host", m_hostname);
579
  if (err != ESP_OK) {
580
    log_error("Failed to save hostname");
581
    nvs_close(nvs_handle);
582
    return false;
583
  }
584
 
585
  // Save number of credentials
586
  err = nvs_set_u8(nvs_handle, "count", m_credentials.size());
587
  if (err != ESP_OK) {
588
    log_error("Failed to save credentials count");
589
    nvs_close(nvs_handle);
590
    return false;
591
  }
592
 
593
  // Save each credential
594
  for (size_t i = 0; i < m_credentials.size(); i++) {
595
    const WiFiCredential &cred = m_credentials[i];
596
 
597
    // SSID key
598
    String key_ssid = "ssid" + String(i);
599
    err = nvs_set_str(nvs_handle, key_ssid.c_str(), cred.ssid);
600
    if (err != ESP_OK) {
601
      log_error("Failed to save SSID %d", i);
602
      nvs_close(nvs_handle);
603
      return false;
604
    }
605
 
606
    // Encrypted password key (as blob)
607
    String key_pass = "pass" + String(i);
608
    err = nvs_set_blob(nvs_handle, key_pass.c_str(), cred.pwd_encrypted, cred.pwd_len);
609
    if (err != ESP_OK) {
610
      log_error("Failed to save password %d", i);
611
      nvs_close(nvs_handle);
612
      return false;
613
    }
614
 
615
    // Password length
616
    String key_len = "len" + String(i);
617
    err = nvs_set_u16(nvs_handle, key_len.c_str(), cred.pwd_len);
618
    if (err != ESP_OK) {
619
      log_error("Failed to save password length %d", i);
620
      nvs_close(nvs_handle);
621
      return false;
622
    }
623
 
624
    // Static IP Configuration - Gateway
625
    String key_gw = "gw" + String(i);
626
    uint32_t gw_addr = cred.gateway;
627
    err = nvs_set_u32(nvs_handle, key_gw.c_str(), gw_addr);
628
    if (err != ESP_OK) {
629
      log_error("Failed to save gateway %d", i);
630
      nvs_close(nvs_handle);
631
      return false;
632
    }
633
 
634
    // Static IP Configuration - Subnet
635
    String key_sn = "sn" + String(i);
636
    uint32_t sn_addr = cred.subnet;
637
    err = nvs_set_u32(nvs_handle, key_sn.c_str(), sn_addr);
638
    if (err != ESP_OK) {
639
      log_error("Failed to save subnet %d", i);
640
      nvs_close(nvs_handle);
641
      return false;
642
    }
643
 
644
    // Static IP Configuration - Local IP
645
    String key_ip = "ip" + String(i);
646
    uint32_t ip_addr = cred.local_ip;
647
    err = nvs_set_u32(nvs_handle, key_ip.c_str(), ip_addr);
648
    if (err != ESP_OK) {
649
      log_error("Failed to save local IP %d", i);
650
      nvs_close(nvs_handle);
651
      return false;
652
    }
653
 
654
    // Static DNS configuration (per credential, optional)
655
    String key_dns1 = "dns1_" + String(i);
656
    uint32_t dns1_addr_cred = cred.dns1;
657
    err = nvs_set_u32(nvs_handle, key_dns1.c_str(), dns1_addr_cred);
658
    if (err != ESP_OK) {
659
      log_error("Failed to save DNS1 %d", i);
660
      nvs_close(nvs_handle);
661
      return false;
662
    }
663
 
664
    String key_dns2 = "dns2_" + String(i);
665
    uint32_t dns2_addr_cred = cred.dns2;
666
    err = nvs_set_u32(nvs_handle, key_dns2.c_str(), dns2_addr_cred);
667
    if (err != ESP_OK) {
668
      log_error("Failed to save DNS2 %d", i);
669
      nvs_close(nvs_handle);
670
      return false;
671
    }
672
  }
673
 
674
  err = nvs_commit(nvs_handle);
675
  nvs_close(nvs_handle);
676
 
677
  if (err == ESP_OK) {
678
    log_info("Credentials saved to NVS (%d entries)", m_credentials.size());
679
    return true;
680
  } else {
681
    log_error("Failed to commit NVS");
682
    return false;
683
  }
684
}
685
 
686
// Load credentials from ESP32 NVS using the layout documented
687
// in saveToNVS(). If any entry is missing, sensible defaults
688
// (e.g. 0.0.0.0 for IP/DNS) are applied.
689
bool CredentialManager::loadFromNVS(const char *nvs_namespace) {
690
  m_credentials.clear();
691
  nvs_handle_t nvs_handle;
692
  esp_err_t err = nvs_open(nvs_namespace, NVS_READONLY, &nvs_handle);
693
 
694
  if (err != ESP_OK) {
695
    log_info("No NVS data found for namespace: %s", nvs_namespace);
696
    return false;
697
  }
698
 
699
  size_t host_len = sizeof(m_hostname);
700
  if (nvs_get_str(nvs_handle, "host", m_hostname, &host_len) != ESP_OK) {
701
    memset(m_hostname, 0, sizeof(m_hostname));
702
  }
703
 
704
  // Read number of credentials
705
  uint8_t count = 0;
706
  err = nvs_get_u8(nvs_handle, "count", &count);
707
  if (err != ESP_OK || count == 0) {
708
    log_debug("No credentials in NVS");
709
    nvs_close(nvs_handle);
710
    return false;
711
  }
712
 
713
  if (count > MAX_CREDENTIALS) {
714
    count = MAX_CREDENTIALS;
715
  }
716
 
717
  // Read each credential
718
  for (int i = 0; i < count; i++) {
719
    WiFiCredential cred;
720
    memset(&cred, 0, sizeof(cred));
721
 
722
    // Read SSID
723
    String key_ssid = "ssid" + String(i);
724
    size_t ssid_len = 33;
725
    err = nvs_get_str(nvs_handle, key_ssid.c_str(), cred.ssid, &ssid_len);
726
    if (err != ESP_OK) {
727
      log_error("Failed to load SSID %d", i);
728
      continue;
729
    }
730
 
731
    // Read password length
732
    String key_len = "len" + String(i);
733
    uint16_t pass_len = 0;
734
    err = nvs_get_u16(nvs_handle, key_len.c_str(), &pass_len);
735
    if (err != ESP_OK || pass_len == 0 || pass_len > 64) {
736
      log_error("Invalid password length for credential %d", i);
737
      continue;
738
    }
739
 
740
    // Read encrypted password
741
    String key_pass = "pass" + String(i);
742
    size_t blob_len = pass_len;
743
    err = nvs_get_blob(nvs_handle, key_pass.c_str(), cred.pwd_encrypted, &blob_len);
744
    if (err != ESP_OK) {
745
      log_error("Failed to load password %d", i);
746
      continue;
747
    }
748
 
749
    cred.pwd_len = pass_len;
750
 
751
    // Read Static IP Configuration - Gateway
752
    String key_gw = "gw" + String(i);
753
    uint32_t gw_addr = 0;
754
    err = nvs_get_u32(nvs_handle, key_gw.c_str(), &gw_addr);
755
    if (err == ESP_OK) {
756
      cred.gateway = IPAddress(gw_addr);
757
    } else {
758
      cred.gateway = IPAddress(0, 0, 0, 0);
759
    }
760
 
761
    // Read Static IP Configuration - Subnet
762
    String key_sn = "sn" + String(i);
763
    uint32_t sn_addr = 0;
764
    err = nvs_get_u32(nvs_handle, key_sn.c_str(), &sn_addr);
765
    if (err == ESP_OK) {
766
      cred.subnet = IPAddress(sn_addr);
767
    } else {
768
      cred.subnet = IPAddress(0, 0, 0, 0);
769
    }
770
 
771
    // Read Static IP Configuration - Local IP
772
    String key_ip = "ip" + String(i);
773
    uint32_t ip_addr = 0;
774
    err = nvs_get_u32(nvs_handle, key_ip.c_str(), &ip_addr);
775
    if (err == ESP_OK) {
776
      cred.local_ip = IPAddress(ip_addr);
777
    } else {
778
      cred.local_ip = IPAddress(0, 0, 0, 0);
779
    }
780
 
781
    // Read Static DNS configuration (per credential, optional)
782
    String key_dns1 = "dns1_" + String(i);
783
    uint32_t dns1_addr_cred = 0;
784
    if (nvs_get_u32(nvs_handle, key_dns1.c_str(), &dns1_addr_cred) == ESP_OK) {
785
      cred.dns1 = IPAddress(dns1_addr_cred);
786
    } else {
787
      cred.dns1 = IPAddress(0, 0, 0, 0);
788
    }
789
 
790
    String key_dns2 = "dns2_" + String(i);
791
    uint32_t dns2_addr_cred = 0;
792
    if (nvs_get_u32(nvs_handle, key_dns2.c_str(), &dns2_addr_cred) == ESP_OK) {
793
      cred.dns2 = IPAddress(dns2_addr_cred);
794
    } else {
795
      cred.dns2 = IPAddress(0, 0, 0, 0);
796
    }
797
 
798
    m_credentials.push_back(cred);
799
  }
800
 
801
  nvs_close(nvs_handle);
802
 
803
  if (m_credentials.size() > 0) {
804
    log_info("Loaded %d credentials from NVS", m_credentials.size());
805
    return true;
806
  }
807
 
808
  return false;
809
}
810
#endif
811
 
812
String CredentialManager::getDebugInfo() const {
813
  String info = "\n=== Credential Manager Debug Info ===\n";
814
 
815
  info += "Encryption Status: " + getStatus() + "\n";
816
  info += "Credentials Count: " + String(m_credentials.size()) + "/" +
817
          String(MAX_CREDENTIALS) + "\n";
818
 
819
#if defined(ESP32)
820
  info += "BLOCK_KEY0 Status: ";
821
 
822
  // Read BLOCK_KEY0 status
823
  uint8_t efuse_key[32];
824
  esp_err_t err = esp_efuse_read_block(EFUSE_BLK_KEY0, efuse_key, 0, 256);
825
 
826
  if (err == ESP_OK) {
827
    bool all_ff = true;
828
    bool all_00 = true;
829
    for (int i = 0; i < 32; i++) {
830
      if (efuse_key[i] != 0xFF)
831
        all_ff = false;
832
      if (efuse_key[i] != 0x00)
833
        all_00 = false;
834
    }
835
 
836
    if (all_ff) {
837
      info += "NOT PROGRAMMED (all 0xFF)\n";
838
    } else if (all_00) {
839
      info += "INVALID (all 0x00)\n";
840
    } else {
841
      info += "PROGRAMMED (bytes present)\n";
842
    }
843
  } else {
844
    info += "ERROR: " + String(esp_err_to_name(err)) + "\n";
845
  }
846
#else
847
  info += "BLOCK_KEY0 Status: N/A (not ESP32)\n";
848
#endif
849
 
850
  info += "\nCredentials:\n";
851
  for (size_t i = 0; i < m_credentials.size(); i++) {
852
    info += "  [" + String(i) + "] SSID: " + String(m_credentials[i].ssid);
853
 
854
    // Show IP configuration if not all zeros
855
    if (m_credentials[i].local_ip != IPAddress(0, 0, 0, 0)) {
856
      info += " | IP: " + m_credentials[i].local_ip.toString();
857
      info += " | GW: " + m_credentials[i].gateway.toString();
858
      info += " | SN: " + m_credentials[i].subnet.toString();
859
    } else {
860
      info += " | IP: DHCP";
861
    }
862
 
863
    info += " | Encrypted len: " + String(m_credentials[i].pwd_len) + "\n";
864
  }
865
 
866
  info += "=====================================\n";
867
  return info;
868
}