Enhance CommonCLI and JWTHelper for MQTT owner public key support

- Added functionality to load, save, and handle the MQTT owner public key in CommonCLI.
- Implemented validation for the owner public key format and updated command handling to set the key.
- Modified JWTHelper to include optional owner and client fields in JWT token creation.
- Increased buffer sizes for JWT tokens in MQTTBridge to accommodate new fields.
This commit is contained in:
agessaman
2026-01-02 13:36:41 -08:00
parent e4ff06cbfd
commit e5dbd56b5d
7 changed files with 296 additions and 14 deletions
+12
View File
@@ -757,6 +757,18 @@ void MyMesh::begin(FILESYSTEM *fs) {
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
// Ensure analyzer servers are enabled by default (in case no prefs were loaded)
if (_prefs.mqtt_analyzer_us_enabled == 0 && _prefs.mqtt_analyzer_eu_enabled == 0) {
_prefs.mqtt_analyzer_us_enabled = 1; // enabled
_prefs.mqtt_analyzer_eu_enabled = 1; // enabled
MESH_DEBUG_PRINTLN("Setting analyzer servers to enabled by default");
}
// Set MQTT origin to actual device name (not build-time ADVERT_NAME)
StrHelper::strncpy(_prefs.mqtt_origin, _prefs.node_name, sizeof(_prefs.mqtt_origin));
MESH_DEBUG_PRINTLN("MQTT origin set to device name: %s", _prefs.mqtt_origin);
acl.load(_fs);
// TODO: key_store.begin();
region_map.load(_fs);
+33
View File
@@ -129,6 +129,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// Let's Mesh Analyzer settings
file.read((uint8_t *)&_prefs->mqtt_analyzer_us_enabled, sizeof(_prefs->mqtt_analyzer_us_enabled)); // 344
file.read((uint8_t *)&_prefs->mqtt_analyzer_eu_enabled, sizeof(_prefs->mqtt_analyzer_eu_enabled)); // 345
file.read((uint8_t *)&_prefs->mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); // 346
// 209
>>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge)
@@ -264,6 +265,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
// Let's Mesh Analyzer settings
file.write((uint8_t *)&_prefs->mqtt_analyzer_us_enabled, sizeof(_prefs->mqtt_analyzer_us_enabled)); // 344
file.write((uint8_t *)&_prefs->mqtt_analyzer_eu_enabled, sizeof(_prefs->mqtt_analyzer_eu_enabled)); // 345
file.write((uint8_t *)&_prefs->mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); // 346
// 209
>>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge)
@@ -494,6 +496,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %s", _prefs->mqtt_analyzer_us_enabled ? "on" : "off");
} else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_analyzer_eu_enabled ? "on" : "off");
} else if (memcmp(config, "mqtt.owner", 10) == 0) {
if (_prefs->mqtt_owner_public_key[0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_owner_public_key);
} else {
strcpy(reply, "> (not set)");
}
} else if (memcmp(config, "mqtt.config.valid", 17) == 0) {
// Check if MQTT configuration is valid using static method
bool valid = MQTTBridge::isConfigValid(_prefs);
@@ -777,6 +785,31 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->mqtt_analyzer_eu_enabled = memcmp(&config[17], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.owner ", 11) == 0) {
// Validate that it's a valid hex string of the correct length (64 hex chars = 32 bytes)
const char* owner_key = &config[11];
int key_len = strlen(owner_key);
if (key_len == 64) {
// Validate hex characters
bool valid = true;
for (int i = 0; i < key_len; i++) {
if (!((owner_key[i] >= '0' && owner_key[i] <= '9') ||
(owner_key[i] >= 'A' && owner_key[i] <= 'F') ||
(owner_key[i] >= 'a' && owner_key[i] <= 'f'))) {
valid = false;
break;
}
}
if (valid) {
StrHelper::strncpy(_prefs->mqtt_owner_public_key, owner_key, sizeof(_prefs->mqtt_owner_public_key));
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: invalid hex characters in public key");
}
} else {
strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)");
}
#endif
} else {
sprintf(reply, "unknown config: %s", config);
-5
View File
@@ -106,17 +106,12 @@ struct MQTTPrefs {
uint16_t mqtt_port; // MQTT server port
char mqtt_username[32]; // MQTT username
char mqtt_password[64]; // MQTT password
=======
>>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge)
// Let's Mesh Analyzer settings
uint8_t mqtt_analyzer_us_enabled; // Enable US analyzer server
uint8_t mqtt_analyzer_eu_enabled; // Enable EU analyzer server
<<<<<<< HEAD
char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key)
char mqtt_email[64]; // Owner email address for matching nodes with owners
=======
>>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge)
};
#endif
+23 -3
View File
@@ -14,7 +14,9 @@ bool JWTHelper::createAuthToken(
unsigned long issuedAt,
unsigned long expiresIn,
char* token,
size_t tokenSize
size_t tokenSize,
const char* owner,
const char* client
) {
Serial.printf("JWTHelper: Starting JWT creation for audience: %s\n", audience);
@@ -52,7 +54,7 @@ bool JWTHelper::createAuthToken(
// Create payload with HEX public key (not base64!)
char payload[512];
size_t payloadLen = createPayload(publicKeyHex, audience, issuedAt, expiresIn, payload, sizeof(payload));
size_t payloadLen = createPayload(publicKeyHex, audience, issuedAt, expiresIn, payload, sizeof(payload), owner, client);
if (payloadLen == 0) {
Serial.printf("JWTHelper: Failed to create payload\n");
return false;
@@ -219,11 +221,19 @@ size_t JWTHelper::createPayload(
unsigned long issuedAt,
unsigned long expiresIn,
char* output,
size_t outputSize
size_t outputSize,
const char* owner,
const char* client
) {
Serial.printf("JWTHelper: createPayload called with outputSize: %d\n", (int)outputSize);
Serial.printf("JWTHelper: publicKey: %s, audience: %s, issuedAt: %lu, expiresIn: %lu\n",
publicKey, audience, issuedAt, expiresIn);
if (owner) {
Serial.printf("JWTHelper: owner: %s\n", owner);
}
if (client) {
Serial.printf("JWTHelper: client: %s\n", client);
}
// Create JWT payload
DynamicJsonDocument doc(512);
@@ -235,6 +245,16 @@ size_t JWTHelper::createPayload(
doc["exp"] = issuedAt + expiresIn;
}
// Add optional owner field if provided
if (owner && strlen(owner) > 0) {
doc["owner"] = owner;
}
// Add optional client field if provided
if (client && strlen(client) > 0) {
doc["client"] = client;
}
// Use temporary buffer for JSON
char jsonBuffer[512];
size_t len = serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
+10 -2
View File
@@ -20,6 +20,8 @@ public:
* @param expiresIn Expiration time in seconds (0 for no expiration)
* @param token Buffer to store the resulting token
* @param tokenSize Size of the token buffer
* @param owner Optional owner public key in hex format (nullptr if not set)
* @param client Optional client string (nullptr if not set)
* @return true if token was created successfully
*/
static bool createAuthToken(
@@ -28,7 +30,9 @@ public:
unsigned long issuedAt = 0,
unsigned long expiresIn = 0,
char* token = nullptr,
size_t tokenSize = 0
size_t tokenSize = 0,
const char* owner = nullptr,
const char* client = nullptr
);
private:
@@ -61,6 +65,8 @@ private:
* @param expiresIn Expiration time in seconds (0 for no expiration)
* @param output Output buffer
* @param outputSize Size of output buffer
* @param owner Optional owner public key in hex format (nullptr if not set)
* @param client Optional client string (nullptr if not set)
* @return Length of payload, or 0 on error
*/
static size_t createPayload(
@@ -69,7 +75,9 @@ private:
unsigned long issuedAt,
unsigned long expiresIn,
char* output,
size_t outputSize
size_t outputSize,
const char* owner = nullptr,
const char* client = nullptr
);
};
+216 -2
View File
@@ -999,12 +999,35 @@ bool MQTTBridge::createAuthToken() {
bool us_token_created = false;
bool eu_token_created = false;
// Get current time for expiration tracking
unsigned long current_time = time(nullptr);
unsigned long expires_in = 86400; // 24 hours
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
MQTT_DEBUG_PRINTLN("Using owner public key: %s", owner_key);
}
// Build client version string (same format as used in status messages)
char client_version[64];
snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date);
// Create JWT token for US server
if (_analyzer_us_enabled) {
MQTT_DEBUG_PRINTLN("Creating JWT token for US server...");
if (JWTHelper::createAuthToken(
*_identity, "mqtt-us-v1.letsmesh.net",
0, 86400, _auth_token_us, sizeof(_auth_token_us))) {
0, expires_in, _auth_token_us, sizeof(_auth_token_us),
owner_key, client_version)) {
MQTT_DEBUG_PRINTLN("Created auth token for US server");
us_token_created = true;
} else {
@@ -1017,7 +1040,8 @@ bool MQTTBridge::createAuthToken() {
MQTT_DEBUG_PRINTLN("Creating JWT token for EU server...");
if (JWTHelper::createAuthToken(
*_identity, "mqtt-eu-v1.letsmesh.net",
0, 86400, _auth_token_eu, sizeof(_auth_token_eu))) {
0, expires_in, _auth_token_eu, sizeof(_auth_token_eu),
owner_key, client_version)) {
MQTT_DEBUG_PRINTLN("Created auth token for EU server");
eu_token_created = true;
} else {
@@ -1302,9 +1326,199 @@ void MQTTBridge::publishStatusToAnalyzerClient(PsychicMqttClient* client, const
}
void MQTTBridge::maintainAnalyzerConnections() {
<<<<<<< HEAD
// PsychicMqttClient handles connection maintenance and reconnection automatically
// No manual maintenance needed - the library manages this internally
// Connection state changes are handled via the onConnect/onDisconnect callbacks
=======
if (!_identity) {
return;
}
unsigned long current_time = time(nullptr);
// If time is not synced (time() returns 0 or very small value), skip expiration checks
// Tokens will still work but we can't track expiration properly
// If expiration time was set before time sync, it will be a small value, so we'll renew
bool time_synced = (current_time >= 1000000000); // After year 2001
const unsigned long RENEWAL_BUFFER = 3600; // Renew tokens 1 hour before expiration
const unsigned long RENEWAL_THROTTLE_MS = 60000; // Don't attempt renewal more than once per minute
unsigned long now_millis = millis();
// Check and renew US server token if needed
if (_analyzer_us_enabled && _analyzer_us_client) {
// Check if token is expired or will expire soon
// If time wasn't synced when token was created, expiration time will be invalid, so renew
bool token_needs_renewal = (_token_us_expires_at == 0) ||
!time_synced ||
!(_token_us_expires_at >= 1000000000) || // Expiration time invalid
(time_synced && current_time >= _token_us_expires_at) ||
(time_synced && current_time >= (_token_us_expires_at - RENEWAL_BUFFER));
// Throttle renewal attempts - don't try more than once per minute to avoid blocking
bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_us) >= RENEWAL_THROTTLE_MS;
// Check if client is disconnected and needs reconnection with new token
bool needs_reconnect = !_analyzer_us_client->connected();
if (token_needs_renewal && can_attempt_renewal) {
_last_token_renewal_attempt_us = now_millis;
MQTT_DEBUG_PRINTLN("US token expired or expiring soon (expires_at: %lu, current: %lu), renewing...",
_token_us_expires_at, current_time);
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
}
// Build client version string (same format as used in status messages)
char client_version[64];
snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date);
// Renew the token
if (JWTHelper::createAuthToken(
*_identity, "mqtt-us-v1.letsmesh.net",
0, 86400, _auth_token_us, sizeof(_auth_token_us),
owner_key, client_version)) {
unsigned long expires_in = 86400; // 24 hours
_token_us_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("US token renewed, new expiration: %lu", _token_us_expires_at);
// Update client credentials with new token
_analyzer_us_client->setCredentials(_analyzer_username, _auth_token_us);
// Reconnect to apply new token (whether currently connected or not)
// If connected, disconnect first to ensure new token is used
if (_analyzer_us_client->connected()) {
MQTT_DEBUG_PRINTLN("Disconnecting US server to apply new token...");
_analyzer_us_client->disconnect();
}
MQTT_DEBUG_PRINTLN("Reconnecting to US server with renewed token...");
_last_reconnect_attempt_us = now_millis; // Update reconnect timestamp to throttle subsequent attempts
_analyzer_us_client->connect();
} else {
MQTT_DEBUG_PRINTLN("Failed to renew US token");
_token_us_expires_at = 0;
}
} else if (needs_reconnect) {
// Token is still valid but connection is lost - reconnect with existing token
// Throttle reconnection attempts to avoid spamming
unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_us) ?
(now_millis - _last_reconnect_attempt_us) :
(ULONG_MAX - _last_reconnect_attempt_us + now_millis + 1);
if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) {
_last_reconnect_attempt_us = now_millis;
MQTT_DEBUG_PRINTLN("US server disconnected but token still valid, reconnecting...");
_analyzer_us_client->connect();
} else {
// Throttled - only log periodically to avoid spam (every 5 minutes max)
static unsigned long last_throttle_log_us = 0;
if (now_millis - last_throttle_log_us > 300000) {
MQTT_DEBUG_PRINTLN("US server reconnection throttled (last attempt %lu ms ago, need %lu ms)",
reconnect_elapsed, RECONNECT_THROTTLE_MS);
last_throttle_log_us = now_millis;
}
}
}
}
// Check and renew EU server token if needed
if (_analyzer_eu_enabled && _analyzer_eu_client) {
// Check if token is expired or will expire soon
// If time wasn't synced when token was created, expiration time will be invalid, so renew
bool token_needs_renewal = (_token_eu_expires_at == 0) ||
!time_synced ||
!(_token_eu_expires_at >= 1000000000) || // Expiration time invalid
(time_synced && current_time >= _token_eu_expires_at) ||
(time_synced && current_time >= (_token_eu_expires_at - RENEWAL_BUFFER));
// Throttle renewal attempts - don't try more than once per minute to avoid blocking
bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_eu) >= RENEWAL_THROTTLE_MS;
// Check if client is disconnected and needs reconnection with new token
bool needs_reconnect = !_analyzer_eu_client->connected();
if (token_needs_renewal && can_attempt_renewal) {
_last_token_renewal_attempt_eu = now_millis;
MQTT_DEBUG_PRINTLN("EU token expired or expiring soon (expires_at: %lu, current: %lu), renewing...",
_token_eu_expires_at, current_time);
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
}
// Build client version string (same format as used in status messages)
char client_version[64];
snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date);
// Renew the token
if (JWTHelper::createAuthToken(
*_identity, "mqtt-eu-v1.letsmesh.net",
0, 86400, _auth_token_eu, sizeof(_auth_token_eu),
owner_key, client_version)) {
unsigned long expires_in = 86400; // 24 hours
_token_eu_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("EU token renewed, new expiration: %lu", _token_eu_expires_at);
// Update client credentials with new token
_analyzer_eu_client->setCredentials(_analyzer_username, _auth_token_eu);
// Reconnect to apply new token (whether currently connected or not)
// If connected, disconnect first to ensure new token is used
if (_analyzer_eu_client->connected()) {
MQTT_DEBUG_PRINTLN("Disconnecting EU server to apply new token...");
_analyzer_eu_client->disconnect();
}
MQTT_DEBUG_PRINTLN("Reconnecting to EU server with renewed token...");
_last_reconnect_attempt_eu = now_millis; // Update reconnect timestamp to throttle subsequent attempts
_analyzer_eu_client->connect();
} else {
MQTT_DEBUG_PRINTLN("Failed to renew EU token");
_token_eu_expires_at = 0;
}
} else if (needs_reconnect) {
// Token is still valid but connection is lost - reconnect with existing token
// Throttle reconnection attempts to avoid spamming
unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_eu) ?
(now_millis - _last_reconnect_attempt_eu) :
(ULONG_MAX - _last_reconnect_attempt_eu + now_millis + 1);
if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) {
_last_reconnect_attempt_eu = now_millis;
MQTT_DEBUG_PRINTLN("EU server disconnected but token still valid, reconnecting...");
_analyzer_eu_client->connect();
} else {
// Throttled - only log periodically to avoid spam (every 5 minutes max)
static unsigned long last_throttle_log_eu = 0;
if (now_millis - last_throttle_log_eu > 300000) {
MQTT_DEBUG_PRINTLN("EU server reconnection throttled (last attempt %lu ms ago, need %lu ms)",
reconnect_elapsed, RECONNECT_THROTTLE_MS);
last_throttle_log_eu = now_millis;
}
}
}
}
// Note: PsychicMqttClient handles automatic reconnection internally,
// but we need to ensure tokens are renewed before reconnection attempts
>>>>>>> 1ebe417c (Enhance CommonCLI and JWTHelper for MQTT owner public key support)
}
void MQTTBridge::setMessageTypes(bool status, bool packets, bool raw) {
+2 -2
View File
@@ -117,8 +117,8 @@ private:
// Let's Mesh Analyzer support
bool _analyzer_us_enabled;
bool _analyzer_eu_enabled;
char _auth_token_us[512]; // JWT token for US server authentication
char _auth_token_eu[512]; // JWT token for EU server authentication
char _auth_token_us[768]; // JWT token for US server authentication (increased for owner/client fields)
char _auth_token_eu[768]; // JWT token for EU server authentication (increased for owner/client fields)
char _analyzer_username[70]; // Username in format v1_{UPPERCASE_PUBLIC_KEY}
// Device identity for JWT token creation