Skip to content

Commit 962a23d

Browse files
committed
- Make it possible to transfer Strings containing null values via ESP-NOW and FloodingMesh.
- Add uint8ArrayToMultiString and bufferedUint8ArrayToMultiString TypeConversionFunctions to facilitate transfer of Strings containing null values. - Add HKDF to CryptoInterface. - Add ChaCha20 + Poly1305 AEAD to CryptoInterface. - Add customizable nonce generator to CryptoInterface. - Add ability to automatically encrypt/decrypt ESP-NOW messages via AEAD (ChaCha20 + Poly1305), independent from encrypted ESP-NOW connections. - Greatly improve performance of incrementSessionKey, espnowGetMessageID, espnowSetMessageID and all non-template TypeConversionFunctions. The average performance increase is roughly a factor 5. Fun fact: Printing a MAC to a HEX String is now over twice as fast when using TypeConversionFunctions compared to using standard functionality like sprintf. - Add uint64ToUint8Array and uint8ArrayToUint64 TypeConversionFunctions. - Make it possible to use String values as ESP-NOW and FloodingMesh key seeds, instead of just requiring plain key arrays. - Add customizable responseTransmittedHook to sendEspnowResponses. - Add _responsesToSendMutex to make the new responseTransmittedHook safe to use. - Remove verboseModePrinting from sendPeerRequestConfirmations method to reduce performance variations. - Fix faulty messageID generation in FloodingMesh. - Make assert checks more complete and easier to understand in the setMetadataDelimiter method of FloodingMesh. - Rename EspnowEncryptionKey to EspnowEncryptedConnectionKey since there are now multiple encryption keys. - Rename acceptsUnencryptedRequests to acceptsUnverifiedRequests, unencryptedMessageID to unsynchronizedMessageID, receivedEncryptedMessage to receivedEncryptedTransmission, since there are now multiple modes of encryption. - Rename resultArrayLength to outputLength in CryptoInterface and remove its value restrictions in order to match the BearSSL functionality. - Improve performance of FloodingMesh::encryptedBroadcast. - Rename FloodingMesh methods maxUnencryptedMessageSize/maxEncryptedMessageSize to maxUnencryptedMessageLength/maxEncryptedMessageLength, so that String length naming is consistent within the library. - Update examples to illustrate the new features. - Improve comments.
1 parent 2fef67d commit 962a23d

20 files changed

+1201
-317
lines changed

libraries/ESP8266WiFiMesh/examples/HelloEspnow/HelloEspnow.ino

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The
1919

2020
// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired.
2121
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.
22-
uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions.
23-
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
24-
};
25-
uint8_t espnowEncryptionKok[16] = {0x22, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting the encryption key.
22+
// Note that it is also possible to use Strings as key seeds instead of arrays.
23+
uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections.
24+
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
25+
};
26+
uint8_t espnowEncryptionKok[16] = {0x22, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting the encrypted connection key.
2627
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x33
2728
};
2829
uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests.
@@ -40,7 +41,7 @@ void networkFilter(int numberOfNetworks, MeshBackendBase &meshInstance);
4041
bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance);
4142

4243
/* Create the mesh node object */
43-
EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, broadcastFilter, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
44+
EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, broadcastFilter, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
4445

4546
/**
4647
Callback for when other nodes send you a request
@@ -57,8 +58,8 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
5758

5859
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
5960
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
60-
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
61-
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
61+
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
62+
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
6263
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
6364
(void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
6465
Serial.print("TCP/IP: ");
@@ -69,6 +70,7 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
6970
/* Print out received message */
7071
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
7172
// If you need to print the whole String it is better to store it and print it in the loop() later.
73+
// Note that request.substring will not work as expected if the String contains null values as data.
7274
Serial.print("Request received: ");
7375
Serial.println(request.substring(0, 100));
7476

@@ -88,8 +90,8 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
8890

8991
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
9092
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
91-
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
92-
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
93+
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
94+
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
9395
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
9496
Serial.print("TCP/IP: ");
9597

@@ -106,6 +108,7 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
106108
/* Print out received message */
107109
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
108110
// If you need to print the whole String it is better to store it and print it in the loop() later.
111+
// Note that response.substring will not work as expected if the String contains null values as data.
109112
Serial.print(F("Response received: "));
110113
Serial.println(response.substring(0, 100));
111114

@@ -169,7 +172,9 @@ bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance)
169172
return false; // Broadcast is for another mesh network
170173
} else {
171174
// Remove metadata from message and mark as accepted broadcast.
172-
firstTransmission = firstTransmission.substring(metadataEndIndex + 1);
175+
// Note that when you modify firstTransmission it is best to avoid using substring or other String methods that rely on null values for String length determination.
176+
// Otherwise your broadcasts cannot include null values in the message bytes.
177+
firstTransmission.remove(0, metadataEndIndex + 1);
173178
return true;
174179
}
175180
}
@@ -193,6 +198,31 @@ bool exampleTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) {
193198
return true;
194199
}
195200

201+
/**
202+
Once passed to the setResponseTransmittedHook method of the ESP-NOW backend,
203+
this function will be called after each successful ESP-NOW response transmission, just before the response is removed from the waiting list.
204+
If a particular response is not sent, there will be no function call for it.
205+
Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called.
206+
207+
@param response The sent response.
208+
@param recipientMac The MAC address the response was sent to.
209+
@param responseIndex The index of the response in the waiting list.
210+
@param meshInstance The EspnowMeshBackend instance that called the function.
211+
212+
@return True if the response transmission process should continue with the next response in the waiting list.
213+
False if the response transmission process should stop after removing the just sent response from the waiting list.
214+
*/
215+
bool exampleResponseTransmittedHook(const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance) {
216+
// Currently this is exactly the same as the default hook, but you can modify it to alter the behaviour of sendEspnowResponses.
217+
218+
(void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
219+
(void)recipientMac;
220+
(void)responseIndex;
221+
(void)meshInstance;
222+
223+
return true;
224+
}
225+
196226
void setup() {
197227
// Prevents the flash memory from being worn out, see: https://github.com/esp8266/Arduino/issues/1054 .
198228
// This will however delay node WiFi start-up by about 700 ms. The delay is 900 ms if we otherwise would have stored the WiFi network we want to connect to.
@@ -222,10 +252,10 @@ void setup() {
222252

223253
// Note: This changes the Kok for all EspnowMeshBackend instances on this ESP8266.
224254
// Encrypted connections added before the Kok change will retain their old Kok.
225-
// Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible.
255+
// Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
226256
// Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
227257
EspnowMeshBackend::setEspnowEncryptionKok(espnowEncryptionKok);
228-
espnowNode.setEspnowEncryptionKey(espnowEncryptionKey);
258+
espnowNode.setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey);
229259

230260
// Makes it possible to find the node through scans, and also makes it possible to recover from an encrypted connection where only the other node is encrypted.
231261
// Note that only one AP can be active at a time in total, and this will always be the one which was last activated.
@@ -238,6 +268,21 @@ void setup() {
238268
espnowNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + espnowNode.getMeshName() + espnowNode.getNodeID() + String(F(".")));
239269

240270
espnowNode.setTransmissionOutcomesUpdateHook(exampleTransmissionOutcomesUpdateHook);
271+
espnowNode.setResponseTransmittedHook(exampleResponseTransmittedHook);
272+
273+
// In addition to using encrypted ESP-NOW connections the framework can also send automatically encrypted messages (AEAD) over both encrypted and unencrypted connections.
274+
// Using AEAD will only encrypt the message content, not the transmission metadata.
275+
// The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer.
276+
// AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections.
277+
// Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption,
278+
// and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission.
279+
// Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established.
280+
//
281+
// Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received.
282+
// All nodes this node wishes to communicate with must then also use encrypted messages with the same getEspnowMessageEncryptionKey(), or messages will not be accepted.
283+
// Note that using AEAD encrypted messages will reduce the number of message bytes that can be transmitted.
284+
//espnowNode.setEspnowMessageEncryptionKey("ChangeThisKeySeed_TODO"); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used.
285+
//espnowNode.setUseEncryptedMessages(true);
241286
}
242287

243288
int32_t timeOfLastScan = -10000;

libraries/ESP8266WiFiMesh/examples/HelloMesh/HelloMesh.ino

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@ const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The
2626

2727
// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired.
2828
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.
29-
uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions.
30-
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
31-
};
29+
// Note that it is also possible to use Strings as key seeds instead of arrays.
30+
uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections.
31+
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
32+
};
3233
uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests.
3334
0x33, 0x44, 0x33, 0xB0, 0x33, 0x44, 0x32, 0xAD
3435
};
3536

3637
bool meshMessageHandler(String &message, FloodingMesh &meshInstance);
3738

3839
/* Create the mesh node object */
39-
FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
40+
FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
4041

4142
bool theOne = true;
4243
String theOneMac = "";
@@ -145,6 +146,13 @@ void setup() {
145146
digitalWrite(LED_BUILTIN, LOW); // Turn LED on (LED_BUILTIN is active low)
146147
}
147148

149+
// Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received via broadcast() and encryptedBroadcast().
150+
// The main benefit of AEAD encryption is that it can be used with normal broadcasts (which are substantially faster than encryptedBroadcasts).
151+
// The main drawbacks are that AEAD only encrypts the message data (not transmission metadata), transfers less data per message and lacks replay attack protection.
152+
// When using AEAD, potential replay attacks must thus be handled manually.
153+
//floodingMesh.getEspnowMeshBackend().setEspnowMessageEncryptionKey("ChangeThisKeySeed_TODO"); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used.
154+
//floodingMesh.getEspnowMeshBackend().setUseEncryptedMessages(true);
155+
148156
floodingMeshDelay(5000); // Give some time for user to start the nodes
149157
}
150158

@@ -171,7 +179,7 @@ void loop() {
171179
uint32_t startTime = millis();
172180
ledState = ledState ^ bool(benchmarkCount); // Make other nodes' LEDs alternate between on and off once benchmarking begins.
173181

174-
// Note: The maximum length of an unencrypted broadcast message is given by floodingMesh.maxUnencryptedMessageSize(). It is around 670 bytes by default.
182+
// Note: The maximum length of an unencrypted broadcast message is given by floodingMesh.maxUnencryptedMessageLength(). It is around 670 bytes by default.
175183
floodingMesh.broadcast(String(floodingMesh.metadataDelimiter()) + String(ledState) + theOneMac + " is The One.");
176184
Serial.println("Proclamation broadcast done in " + String(millis() - startTime) + " ms.");
177185

libraries/ESP8266WiFiMesh/examples/HelloTcpIp/HelloTcpIp.ino

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
4242

4343
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
4444
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
45-
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
46-
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
45+
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
46+
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
4747
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
4848
(void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
4949
Serial.print("TCP/IP: ");
@@ -54,6 +54,7 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
5454
/* Print out received message */
5555
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
5656
// If you need to print the whole String it is better to store it and print it in the loop() later.
57+
// Note that request.substring will not work as expected if the String contains null values as data.
5758
Serial.print("Request received: ");
5859
Serial.println(request.substring(0, 100));
5960

@@ -73,8 +74,8 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
7374

7475
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
7576
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
76-
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
77-
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
77+
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
78+
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
7879
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
7980
Serial.print("TCP/IP: ");
8081

@@ -84,14 +85,14 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
8485
// So for ESP-NOW, adding unique identifiers in the response and request is required to associate a response with a request.
8586
Serial.print(F("Request sent: "));
8687
Serial.println(tcpIpInstance->getCurrentMessage().substring(0, 100));
87-
8888
} else {
8989
Serial.print("UNKNOWN!: ");
9090
}
9191

9292
/* Print out received message */
9393
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
9494
// If you need to print the whole String it is better to store it and print it in the loop() later.
95+
// Note that response.substring will not work as expected if the String contains null values as data.
9596
Serial.print(F("Response received: "));
9697
Serial.println(response.substring(0, 100));
9798

0 commit comments

Comments
 (0)