Share the saved admin password across ESP32 management services

Use the committed node credential for setup WiFi, WebConfig and browser OTA. Add persistent ESP32 WiFi Companion management credentials without changing preferences layouts, revoke stale browser sessions on rotation, retain scoped save receipts, and coordinate credential changes with flash uploads. Keep external-service credentials and trusted TCP protocol compatibility independent. Update live join QR codes, failure handling and documentation.

Validated 51 native cases, 158 related host and browser methods, and serial Full V4 Companion, Station G3 Repeater, ESP32-C6 Room Server and V4 Sensor firmware builds.
This commit is contained in:
mikecarper committed 2026-10-18 13:09:48 -07:00
1 parent a6a3cb5ebc
commit cd02e55824
58 files changed
+5749 -2439

No files matched your search

+92
View File
@@ -40,6 +40,10 @@
#include <helpers/StatsFormatHelper.h>
#include <helpers/UsbAsciiBinarySwitch.h>
#include <helpers/UsbLogging.h>
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
#include <helpers/esp32/CompanionAdminPassword.h>
#include <helpers/esp32/ManagementPassword.h>
#endif
#include <helpers/UsbLoggingWatchdog.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
@@ -3008,6 +3012,23 @@ void MyMesh::begin(bool has_display, bool radio_available,
// load persisted prefs
const bool prefs_ready =
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
#ifdef ADMIN_PASSWORD
const char* management_default = ADMIN_PASSWORD;
#else
const char* management_default = "password";
#endif
_management_password_ready = mesh::esp32::companionAdminPassword::load(
_management_password, management_default);
if (_management_password_ready) {
_management_password_ready =
mesh::esp32::managementPassword::publish(_management_password);
}
if (!_management_password_ready) {
mesh::usbLoggingPort().println(
"Management password unavailable; use password <new-password> on USB to repair");
}
#endif
#if defined(RAK_4631)
// Only a verified image (or a fresh missing file) may permit GPS-only power.
// A failed load never replaces a stored shared policy with an unsafe cut.
@@ -3798,6 +3819,39 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
size_t reply_size) {
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (strncmp(command, "password", 8) == 0
&& (command[8] == 0 || command[8] == ' ' || command[8] == '\t')) {
const char* value = command + 8;
// Consume the command separator only. Extra spaces belong to the secret,
// exactly as in infrastructure CLI and browser password forms.
if (*value == ' ' || *value == '\t') ++value;
if (!mesh::esp32::managementPassword::validPassword(value)) {
snprintf(reply, reply_size,
"Error: password must be 1-15 bytes, no line breaks; use 8-15 ASCII for setup WiFi");
return true;
}
mesh::esp32::managementPassword::ChangeGuard change;
if (!change.acquired()) {
snprintf(reply, reply_size, "Error: OTA upload or password change busy; retry");
return true;
}
if (!mesh::esp32::companionAdminPassword::save(value)) {
snprintf(reply, reply_size, "Error: password save unconfirmed; reboot may differ");
return true;
}
memcpy(_management_password, value, strlen(value) + 1);
_management_password_ready = change.publish(_management_password);
if (!_management_password_ready) {
snprintf(reply, reply_size, "Error: password saved; management unavailable, reboot required");
} else {
// The browser batch recognizes this established success prefix and
// removes the secret before publishing its result.
snprintf(reply, reply_size, "password now: %s", value);
}
return true;
}
#endif
if (mesh::powerdiag::handleCommand(command, reply, reply_size, millis())) return true;
#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800)
if (handleRak4631PeripheralCommand(command, reply, reply_size)) return true;
@@ -3935,6 +3989,13 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
// Binary Companion callers may supply a shorter buffer.
char ota_reply[160] = {0};
bool webconfig_stopped = false;
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (!_management_password_ready
|| !mesh::esp32::managementPassword::publish(_management_password)) {
snprintf(reply, reply_size, "Error: management password unavailable; set password on USB");
return true;
}
#endif
#ifdef WITH_WEBCONFIG
if (isWebConfigActiveOrStopping()) {
if (!_webconfig->stopForOTA(ota_reply)) {
@@ -5034,6 +5095,11 @@ static bool wcCopyValue(char* dest, size_t dest_size, const char* value) {
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
memset(&s, 0, sizeof(s));
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (_management_password_ready) {
wcCopyValue(s.admin_password, sizeof(s.admin_password), _management_password);
}
#endif
wcCopyValue(s.name, sizeof(s.name), _prefs.node_name);
wcCopyValue(s.bluetooth_name, sizeof(s.bluetooth_name),
_prefs.bluetooth_name);
@@ -5106,6 +5172,12 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (!_management_password_ready) {
strcpy(reply, "Err: management password unavailable; set password on USB");
return false;
}
#endif
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE)
if (!isCompanionWiFiEnabled()) {
strcpy(reply,
@@ -5222,6 +5294,13 @@ void MyMesh::execAdminCommand(char* cmd, char* reply) {
void MyMesh::execCommand(char* cmd, char* reply) {
reply[0] = 0;
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (cmd && strncmp(cmd, "password", 8) == 0
&& (cmd[8] == 0 || cmd[8] == ' ' || cmd[8] == '\t')) {
handleLocalControlCommand(cmd, reply, 160);
return;
}
#endif
#ifdef WITH_MQTT_BRIDGE
if (cmd && strncmp(cmd, "set mqtt.enabled ", 17) == 0) {
handleLocalControlCommand(cmd, reply, 160);
@@ -10230,7 +10309,12 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
"Commands:\r\n"
" stats-core / stats-radio / stats-radio-diag / stats-packets\r\n"
" get prv.key (when private key export is enabled)\r\n"
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
" password <new-password> (shared setup WiFi, WebConfig and WiFi OTA)\r\n"
" get password (management password status)\r\n"
#else
" get password (this role has no admin password)\r\n"
#endif
" erase (erase stored settings and identity)\r\n"
" board\r\n"
" version\r\n");
@@ -10514,7 +10598,15 @@ bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_
return true;
}
if (strcmp(command, "get password") == 0) {
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
// Binary Companion and its legacy TCP transports have no admin-login
// handshake. Report status there, never disclose the management secret.
snprintf(reply, reply_size, _management_password_ready
? "> (management password set; use password <new-password> to change)"
: "Error: management password unavailable; set password on USB");
#else
snprintf(reply, reply_size, "> (no admin password on Companion)");
#endif
return true;
}
if (strcmp(command, "get prv.key") == 0) {
+8
View File
@@ -246,6 +246,10 @@ public:
}
#endif
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
bool isManagementPasswordReady() const { return _management_password_ready; }
#endif
#ifdef WITH_WEBCONFIG
bool startWebConfig(bool force_ap, char* reply);
void stopWebConfig();
@@ -794,6 +798,10 @@ private:
#endif
uint16_t _temp_radio_preamble = 0;
CompanionNodePrefs _prefs;
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
char _management_password[16] = {};
bool _management_password_ready = false;
#endif
#if COMPANION_FEATURE_NOTIFICATIONS
mesh::notify::Controller _notifications{*this, *this};
bool _notification_button_down = false;
+2 -6
View File
@@ -306,12 +306,8 @@ static void drawCompanionWiFiSetupPage(DisplayDriver& display) {
display.drawTextCentered(display.width() / 2, 37, setup_ssid);
char payload[256];
const char* setup_password = nullptr;
#ifdef WEBCONFIG_AP_PASSWORD
setup_password = WEBCONFIG_AP_PASSWORD;
#endif
if (mesh::ui::buildWiFiSetupQrPayload(
payload, sizeof(payload), setup_ssid, setup_password)) {
if (mesh::ui::buildCurrentWiFiSetupQrPayload(
payload, sizeof(payload), setup_ssid)) {
static constexpr int qr_size = 105;
const int qr_x = (display.width() - qr_size) / 2;
if (display.drawQrCode(payload, qr_x, 55, qr_size)) {
+14
View File
@@ -11903,6 +11903,20 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
// same over LoRa and serial. Keep leading whitespace intact until after the
// region-load handler because it encodes region hierarchy indentation.
char* command_end = command + strlen(command);
#ifdef ESP_PLATFORM
// Password bytes are significant, including spaces and invalid line breaks
// that the ESP32 setter must reject rather than silently remove.
const char* password_command = command;
while (*password_command == ' ') password_command++;
if (strlen(password_command) > 4 && password_command[2] == '|') {
password_command += 3;
}
char command_verb[10];
strncpy(command_verb, password_command, sizeof(command_verb) - 1);
command_verb[sizeof(command_verb) - 1] = 0;
mesh::cli::normalizeCommandVerb(command_verb);
if (strcmp(command_verb, "password ") != 0)
#endif
while (command_end > command
&& (command_end[-1] == ' ' || command_end[-1] == '\t'
|| command_end[-1] == '\r' || command_end[-1] == '\n')) {