Skip to content

Commit

Permalink
v1.2.43
Browse files Browse the repository at this point in the history
- MQTT client: implemented End To End message Encryption (E2EE)
- Fix bug in JSON-serialization of string values due to missing escaping of special characters
  • Loading branch information
genemars committed Dec 14, 2024
1 parent d4522fe commit 77dc09f
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 46 deletions.
12 changes: 8 additions & 4 deletions src/HomeGenie.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -476,16 +476,18 @@ namespace Service {
"FieldType": "%s",
"UpdateTime": "%s"
})";
auto v = String(value);
Utility::simpleJsonStringEscape(v);
ssize_t size = snprintf(nullptr, 0, parameterTemplate,
name, value, "", "", timestamp
name, v.c_str(), "", "", timestamp
) + 1;
#ifdef BOARD_HAS_PSRAM
char *parameterJson = (char *) ps_malloc(size);
#else
char *parameterJson = (char *) malloc(size);
#endif
snprintf(parameterJson, size, parameterTemplate,
name, value, "", "", timestamp
name, v.c_str(), "", "", timestamp
);
return parameterJson;
}
Expand All @@ -500,8 +502,10 @@ namespace Service {
"Address": "%s",
"Properties": [%s]
})";
auto d = String(description);
Utility::simpleJsonStringEscape(d);
ssize_t size = snprintf(nullptr, 0, moduleTemplate,
name, description, deviceType,
name, d.c_str(), deviceType,
domain, address,
parameters
) + 1;
Expand All @@ -511,7 +515,7 @@ namespace Service {
char *moduleJson = (char *) malloc(size);
#endif
snprintf(moduleJson, size, moduleTemplate,
name, description, deviceType,
name, d.c_str(), deviceType,
domain, address,
parameters
);
Expand Down
18 changes: 18 additions & 0 deletions src/Utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,21 @@ time_t Utility::relativeUtcHoursToLocalTime(double relativeHours, time_t dt) {
// TODO: should also set DST??
return mktime(tm_struct);
}

void Utility::simpleJsonStringEscape(String& s) {
s.replace("\"", "\\\"");
s.replace("\\", "\\\\");
s.replace("\b", "\\b");
s.replace("\f", "\\f");
s.replace("\n", "\\n");
s.replace("\r", "\\r");
s.replace("\t", "\\t");
for (int c = 0; c < s.length(); c++) {
auto ch = s.charAt(c);
if ('\x00' <= ch && ch <= '\x1f') {
char escaped[6];
sprintf(escaped, "\\u%4d", ch);
s = s.substring(0, c - 1) + escaped + s.substring(c);
}
}
}
1 change: 1 addition & 0 deletions src/Utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class Utility {
static ColorHSV rgb2hsv(float r, float g, float b);
static uint32_t getFreeMem();
static time_t relativeUtcHoursToLocalTime(double relativeHours, time_t time);
static void simpleJsonStringEscape(String& s);
static bool isNumeric(const char* s) {
return strlen(s) > 0 && strspn(s, "0123456789.") == strlen(s);
}
Expand Down
2 changes: 2 additions & 0 deletions src/net/MQTTClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

namespace Net {
MQTTRequestHandler* MQTTClient::requestHandler = nullptr;
bool MQTTClient::enableEncryption = false;
String MQTTClient::encryptionKey;
}

#endif
154 changes: 114 additions & 40 deletions src/net/MQTTClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,46 +27,20 @@
#ifndef HOMEGENIE_MINI_MQTTCLIENT_H
#define HOMEGENIE_MINI_MQTTCLIENT_H

#include <mbedtls/base64.h>
#include <mqtt_client.h>
#include <LinkedList.h>


#include "MQTTChannel.h"
#include "Task.h"
#include "data/Module.h"
#include "service/api/APIRequest.h"

#include "./mqtt/MQTTChannel.h"

#include "esp_crt_bundle.h"

// TODO: TLS/SSL support to be completed

static const char *mosquitto_org_pem PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----
MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL
BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG
A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU
BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv
by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE
BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES
MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp
dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg
UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW
Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA
s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH
3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo
E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT
MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV
6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC
6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf
+pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK
sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839
LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE
m/XriWr/Cq4h/JfB7NTsezVslgkBaoU=
-----END CERTIFICATE-----)EOF";


namespace Net {
using namespace Data;
using namespace Service::API;
Expand Down Expand Up @@ -103,6 +77,8 @@ namespace Net {
auto webSockets = String();
username = "";
password = "";
encryptionKey = "";
enableEncryption = false;
for (ModuleParameter* p: parameters) {
if(p->name.equals("ConfigureOptions.ServerAddress")) {
address = String(p->value);
Expand All @@ -116,6 +92,12 @@ namespace Net {
username = String(p->value);
} else if (p->name.equals("ConfigureOptions.Password")) {
password = String(p->value);
} else if (p->name.equals("ConfigureOptions.Encryption")) {
String enable = String(p->value);
enable.toLowerCase();
enableEncryption = enable.equals("on");
} else if (p->name.equals("ConfigureOptions.EncryptionKey")) {
encryptionKey = String(p->value);
}
}

Expand Down Expand Up @@ -144,13 +126,6 @@ namespace Net {
void start() {

if (!clientStarted && ESP_WIFI_STATUS == WL_CONNECTED) {

/*
mbedtls_ssl_config conf;
mbedtls_ssl_config_init(&conf);
arduino_esp_crt_bundle_attach(&conf);
*/

esp_mqtt_client_destroy(client);
client = esp_mqtt_client_init(&mqtt_cfg);
if (client != nullptr) {
Expand All @@ -177,10 +152,64 @@ namespace Net {
}

void broadcast(String *topic, String *payload) override {
if (enableEncryption) {
int ci = topic->indexOf("/");
if (ci > 0) {
auto cid = topic->substring(0, ci);
auto cmd = topic->substring(ci + 1);
encryptTopic(&cid);
encryptTopic(&cmd);
*topic = cid + String("/") + cmd;
decryptTopic(&cmd);
}
encryptionFilter(payload);
}
esp_mqtt_client_publish(client, topic->c_str(), payload->c_str(), (uint16_t)payload->length(), 0, 0);
}

static void encryptTopic(String* topic) {
encryptionFilter(topic);

size_t encLength = topic->length() * 4;
unsigned char encryptedTopic[encLength];
size_t length;
mbedtls_base64_encode(encryptedTopic, encLength, &length,
(const uint8_t *)topic->c_str(), topic->length());

*topic = String(encryptedTopic, length);
topic->replace("/", "%");
topic->replace("+", "&");
}

static void decryptTopic(String* topic) {
topic->replace("%", "/");
topic->replace("&", "+");

size_t encLength = topic->length();
uint8_t encryptedTopic[encLength];
size_t length;
if (mbedtls_base64_decode(encryptedTopic, encLength, &length,
(const uint8_t*)topic->c_str(),
topic->length()) == 0) {
*topic = String(encryptedTopic, length);
encryptionFilter(topic);
}
}

static void encryptionFilter(String* payload) {
if (!encryptionKey.isEmpty()) {
uint8_t output[256];
size_t length;
mbedtls_base64_decode(output, 256, &length, (const uint8_t*)encryptionKey.c_str(), encryptionKey.length());
auto s = String((const char*)output, length);
andFilter(&s, &Config::system.id);
xorFilter(payload, &s);
}
}

private:
static String encryptionKey;
static bool enableEncryption;
String brokerUrl;
String username;
String password;
Expand All @@ -190,6 +219,28 @@ namespace Net {
esp_mqtt_client_handle_t client = nullptr;
esp_mqtt_client_config_t mqtt_cfg { .uri = "" };

static void xorFilter(String* payload, String* clientKey = 0) {
if (!clientKey->isEmpty()) {
for (int c = 0; c < payload->length(); c++) {
auto key = clientKey->charAt(c % clientKey->length());
auto in = (byte) payload->charAt(c);
auto out = key ^ in;
payload->setCharAt(c, out);
}
}
}

static void andFilter(String* payload, String* clientKey = 0) {
if (!clientKey->isEmpty()) {
for (int c = 0; c < payload->length(); c++) {
auto key = clientKey->charAt(c % clientKey->length());
auto in = (byte) payload->charAt(c);
auto out = key & in;
payload->setCharAt(c, out);
}
}
}

/*
* @brief Event handler registered to receive MQTT events
*
Expand All @@ -202,17 +253,24 @@ namespace Net {
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
String topic = Config::system.id + "/#";

// Event dispatched from event loop `base` with `event_id`
auto event = static_cast<esp_mqtt_event_handle_t>(event_data);
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id) {

case MQTT_EVENT_CONNECTED:
esp_mqtt_client_subscribe(client, topic.c_str(), 1);
break;
case MQTT_EVENT_CONNECTED: {
if (enableEncryption) {
auto cid = String(Config::system.id);
encryptTopic(&cid);
cid += "/#";
esp_mqtt_client_subscribe(client, cid.c_str(), 1);
} else {
String topic = Config::system.id + "/#";
esp_mqtt_client_subscribe(client, topic.c_str(), 1);
}
} break;

case MQTT_EVENT_DISCONNECTED:
break;
Expand All @@ -224,7 +282,20 @@ namespace Net {
break;

case MQTT_EVENT_DATA: {
auto t = String(event->topic) + "/";
auto t = String(event->topic);
if (enableEncryption) {
int i = t.indexOf('/');
if (i > 0) {
auto cid = t.substring(0, i);
decryptTopic(&cid);
auto payload = t.substring(i + 1);
decryptTopic(&payload);
t = cid + "/" + payload;
} else {
decryptTopic(&t);
}
}
t += "/";
String cid;
String domain;
String address;
Expand Down Expand Up @@ -252,6 +323,9 @@ namespace Net {
if (cid == Config::system.id && domain == "MQTT.Listeners") {
if (address == Config::system.id) {
auto jsonRequest = String(event->data, event->data_len);
if (enableEncryption) {
encryptionFilter(&jsonRequest);
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, jsonRequest);
if (error.code()) break;
Expand Down
2 changes: 1 addition & 1 deletion src/net/MQTTServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
#include <io/Logger.h>
#include <WiFiServer.h>

#include "MQTTChannel.h"
#include "Task.h"
#include "net/mqtt/MQTTBrokerMini.h"

#include "./mqtt/MQTTChannel.h"

namespace Net {
using namespace MQTT;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

#define VERSION_MAJOR 1
#define VERSION_MINOR 2
#define VERSION_PATCH 42
#define VERSION_PATCH 43

#define STRING_VALUE(...) STRING_VALUE__(__VA_ARGS__)
#define STRING_VALUE__(...) #__VA_ARGS__
Expand Down

0 comments on commit 77dc09f

Please sign in to comment.