1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-06-03 07:02:28 +03:00

- 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.
This commit is contained in:
Anders 2019-12-04 02:30:16 +01:00
parent 2fef67dcb0
commit 962a23d253
20 changed files with 1202 additions and 318 deletions

View File

@ -19,10 +19,11 @@ const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The
// 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.
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.
uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
};
uint8_t espnowEncryptionKok[16] = {0x22, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting the encryption key.
// Note that it is also possible to use Strings as key seeds instead of arrays.
uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
};
uint8_t espnowEncryptionKok[16] = {0x22, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting the encrypted connection key.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x33
};
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);
bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance);
/* Create the mesh node object */
EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, broadcastFilter, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
EspnowMeshBackend espnowNode = EspnowMeshBackend(manageRequest, manageResponse, networkFilter, broadcastFilter, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
/**
Callback for when other nodes send you a request
@ -57,8 +58,8 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
(void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
Serial.print("TCP/IP: ");
@ -69,6 +70,7 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
/* Print out received message */
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
// If you need to print the whole String it is better to store it and print it in the loop() later.
// Note that request.substring will not work as expected if the String contains null values as data.
Serial.print("Request received: ");
Serial.println(request.substring(0, 100));
@ -88,8 +90,8 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
Serial.print("TCP/IP: ");
@ -106,6 +108,7 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
/* Print out received message */
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
// If you need to print the whole String it is better to store it and print it in the loop() later.
// Note that response.substring will not work as expected if the String contains null values as data.
Serial.print(F("Response received: "));
Serial.println(response.substring(0, 100));
@ -169,7 +172,9 @@ bool broadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance)
return false; // Broadcast is for another mesh network
} else {
// Remove metadata from message and mark as accepted broadcast.
firstTransmission = firstTransmission.substring(metadataEndIndex + 1);
// 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.
// Otherwise your broadcasts cannot include null values in the message bytes.
firstTransmission.remove(0, metadataEndIndex + 1);
return true;
}
}
@ -193,6 +198,31 @@ bool exampleTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance) {
return true;
}
/**
Once passed to the setResponseTransmittedHook method of the ESP-NOW backend,
this function will be called after each successful ESP-NOW response transmission, just before the response is removed from the waiting list.
If a particular response is not sent, there will be no function call for it.
Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called.
@param response The sent response.
@param recipientMac The MAC address the response was sent to.
@param responseIndex The index of the response in the waiting list.
@param meshInstance The EspnowMeshBackend instance that called the function.
@return True if the response transmission process should continue with the next response in the waiting list.
False if the response transmission process should stop after removing the just sent response from the waiting list.
*/
bool exampleResponseTransmittedHook(const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance) {
// Currently this is exactly the same as the default hook, but you can modify it to alter the behaviour of sendEspnowResponses.
(void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
(void)recipientMac;
(void)responseIndex;
(void)meshInstance;
return true;
}
void setup() {
// Prevents the flash memory from being worn out, see: https://github.com/esp8266/Arduino/issues/1054 .
// 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() {
// Note: This changes the Kok for all EspnowMeshBackend instances on this ESP8266.
// Encrypted connections added before the Kok change will retain their old Kok.
// Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible.
// Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
// Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
EspnowMeshBackend::setEspnowEncryptionKok(espnowEncryptionKok);
espnowNode.setEspnowEncryptionKey(espnowEncryptionKey);
espnowNode.setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey);
// 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.
// 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() {
espnowNode.setMessage(String(F("Hello world request #")) + String(requestNumber) + String(F(" from ")) + espnowNode.getMeshName() + espnowNode.getNodeID() + String(F(".")));
espnowNode.setTransmissionOutcomesUpdateHook(exampleTransmissionOutcomesUpdateHook);
espnowNode.setResponseTransmittedHook(exampleResponseTransmittedHook);
// In addition to using encrypted ESP-NOW connections the framework can also send automatically encrypted messages (AEAD) over both encrypted and unencrypted connections.
// Using AEAD will only encrypt the message content, not the transmission metadata.
// The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer.
// AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections.
// Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption,
// and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission.
// Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established.
//
// Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received.
// All nodes this node wishes to communicate with must then also use encrypted messages with the same getEspnowMessageEncryptionKey(), or messages will not be accepted.
// Note that using AEAD encrypted messages will reduce the number of message bytes that can be transmitted.
//espnowNode.setEspnowMessageEncryptionKey("ChangeThisKeySeed_TODO"); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used.
//espnowNode.setUseEncryptedMessages(true);
}
int32_t timeOfLastScan = -10000;

View File

@ -26,9 +26,10 @@ const char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The
// 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.
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.
uint8_t espnowEncryptionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
};
// Note that it is also possible to use Strings as key seeds instead of arrays.
uint8_t espnowEncryptedConnectionKey[16] = {0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x33, 0x44, // This is the key for encrypting transmissions of encrypted connections.
0x33, 0x44, 0x33, 0x44, 0x33, 0x44, 0x32, 0x11
};
uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, // This is the secret key used for HMAC during encrypted connection requests.
0x33, 0x44, 0x33, 0xB0, 0x33, 0x44, 0x32, 0xAD
};
@ -36,7 +37,7 @@ uint8_t espnowHashKey[16] = {0xEF, 0x44, 0x33, 0x0C, 0x33, 0x44, 0xFE, 0x44, //
bool meshMessageHandler(String &message, FloodingMesh &meshInstance);
/* Create the mesh node object */
FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
FloodingMesh floodingMesh = FloodingMesh(meshMessageHandler, FPSTR(exampleWiFiPassword), espnowEncryptedConnectionKey, espnowHashKey, FPSTR(exampleMeshName), uint64ToString(ESP.getChipId()), true);
bool theOne = true;
String theOneMac = "";
@ -145,6 +146,13 @@ void setup() {
digitalWrite(LED_BUILTIN, LOW); // Turn LED on (LED_BUILTIN is active low)
}
// Uncomment the lines below to use automatic AEAD encryption/decryption of messages sent/received via broadcast() and encryptedBroadcast().
// The main benefit of AEAD encryption is that it can be used with normal broadcasts (which are substantially faster than encryptedBroadcasts).
// The main drawbacks are that AEAD only encrypts the message data (not transmission metadata), transfers less data per message and lacks replay attack protection.
// When using AEAD, potential replay attacks must thus be handled manually.
//floodingMesh.getEspnowMeshBackend().setEspnowMessageEncryptionKey("ChangeThisKeySeed_TODO"); // The message encryption key should always be set manually. Otherwise a default key (all zeroes) is used.
//floodingMesh.getEspnowMeshBackend().setUseEncryptedMessages(true);
floodingMeshDelay(5000); // Give some time for user to start the nodes
}
@ -171,7 +179,7 @@ void loop() {
uint32_t startTime = millis();
ledState = ledState ^ bool(benchmarkCount); // Make other nodes' LEDs alternate between on and off once benchmarking begins.
// Note: The maximum length of an unencrypted broadcast message is given by floodingMesh.maxUnencryptedMessageSize(). It is around 670 bytes by default.
// Note: The maximum length of an unencrypted broadcast message is given by floodingMesh.maxUnencryptedMessageLength(). It is around 670 bytes by default.
floodingMesh.broadcast(String(floodingMesh.metadataDelimiter()) + String(ledState) + theOneMac + " is The One.");
Serial.println("Proclamation broadcast done in " + String(millis() - startTime) + " ms.");

View File

@ -42,8 +42,8 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
(void)tcpIpInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
Serial.print("TCP/IP: ");
@ -54,6 +54,7 @@ String manageRequest(const String &request, MeshBackendBase &meshInstance) {
/* Print out received message */
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
// If you need to print the whole String it is better to store it and print it in the loop() later.
// Note that request.substring will not work as expected if the String contains null values as data.
Serial.print("Request received: ");
Serial.println(request.substring(0, 100));
@ -73,8 +74,8 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
// To get the actual class of the polymorphic meshInstance, do as follows (meshBackendCast replaces dynamic_cast since RTTI is disabled)
if (EspnowMeshBackend *espnowInstance = meshBackendCast<EspnowMeshBackend *>(&meshInstance)) {
String messageEncrypted = espnowInstance->receivedEncryptedMessage() ? ", Encrypted" : ", Unencrypted";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + messageEncrypted + "): ");
String transmissionEncrypted = espnowInstance->receivedEncryptedTransmission() ? ", Encrypted transmission" : ", Unencrypted transmission";
Serial.print("ESP-NOW (" + espnowInstance->getSenderMac() + transmissionEncrypted + "): ");
} else if (TcpIpMeshBackend *tcpIpInstance = meshBackendCast<TcpIpMeshBackend *>(&meshInstance)) {
Serial.print("TCP/IP: ");
@ -84,7 +85,6 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
// So for ESP-NOW, adding unique identifiers in the response and request is required to associate a response with a request.
Serial.print(F("Request sent: "));
Serial.println(tcpIpInstance->getCurrentMessage().substring(0, 100));
} else {
Serial.print("UNKNOWN!: ");
}
@ -92,6 +92,7 @@ transmission_status_t manageResponse(const String &response, MeshBackendBase &me
/* Print out received message */
// Only show first 100 characters because printing a large String takes a lot of time, which is a bad thing for a callback function.
// If you need to print the whole String it is better to store it and print it in the loop() later.
// Note that response.substring will not work as expected if the String contains null values as data.
Serial.print(F("Response received: "));
Serial.println(response.substring(0, 100));

View File

@ -34,11 +34,64 @@ namespace
size_t _ctMaxDataLength = 1024;
bool _warningsEnabled = true;
br_hkdf_context _storedHkdfContext;
bool _hkdfContextStored = false;
void *createBearsslHmac(const br_hash_class *hashType, const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
uint8_t *defaultNonceGenerator(uint8_t *nonceArray, const size_t nonceLength)
{
assert(1 <= resultArrayLength);
/**
* The ESP32 Technical Reference Manual v4.1 chapter 24 has the following to say about random number generation (no information found for ESP8266):
*
* "When used correctly, every 32-bit value the system reads from the RNG_DATA_REG register of the random number generator is a true random number.
* These true random numbers are generated based on the noise in the Wi-Fi/BT RF system.
* When Wi-Fi and BT are disabled, the random number generator will give out pseudo-random numbers.
*
* When Wi-Fi or BT is enabled, the random number generator is fed two bits of entropy every APB clock cycle (normally 80 MHz).
* Thus, for the maximum amount of entropy, it is advisable to read the random register at a maximum rate of 5 MHz.
* A data sample of 2 GB, read from the random number generator with Wi-Fi enabled and the random register read at 5 MHz,
* has been tested using the Dieharder Random Number Testsuite (version 3.31.1).
* The sample passed all tests."
*
* Since ESP32 is the sequal to ESP8266 it is unlikely that the ESP8266 is able to generate random numbers more quickly than 5 MHz when run at a 80 MHz frequency.
* A maximum random number frequency of 0.5 MHz is used here to leave some margin for possibly inferior components in the ESP8266.
* It should be noted that the ESP8266 has no Bluetooth functionality, so turning the WiFi off is likely to cause RANDOM_REG32 to use pseudo-random numbers.
*
* It is possible that yield() must be called on the ESP8266 to properly feed the hardware random number generator new bits, since there is only one processor core available.
* However, no feeding requirements are mentioned in the ESP32 documentation, and using yield() could possibly cause extended delays during nonce generation.
* Thus only delayMicroseconds() is used below.
*/
constexpr uint8_t cooldownMicros = 2;
static uint32_t lastCalledMicros = micros() - cooldownMicros;
uint32_t randomNumber = 0;
for(size_t byteIndex = 0; byteIndex < nonceLength; ++byteIndex)
{
if(byteIndex % 4 == 0)
{
// Old random number has been used up (random number could be exactly 0, so we can't check for that)
uint32_t timeSinceLastCall = micros() - lastCalledMicros;
if(timeSinceLastCall < cooldownMicros)
delayMicroseconds(cooldownMicros - timeSinceLastCall);
randomNumber = RANDOM_REG32;
lastCalledMicros = micros();
}
nonceArray[byteIndex] = randomNumber;
randomNumber >>= 8;
}
return nonceArray;
}
CryptoInterface::nonceGeneratorType _nonceGenerator = defaultNonceGenerator;
void *createBearsslHmac(const br_hash_class *hashType, const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
// Comments mainly from https://www.bearssl.org/apidoc/bearssl__hmac_8h.html
// HMAC is initialized with a key and an underlying hash function; it then fills a "key context". That context contains the processed key.
@ -54,8 +107,8 @@ namespace
// Initialise a HMAC context with a key context. The key context is unmodified.
// Relevant data from the key context is immediately copied; the key context can thus be independently reused, modified or released without impacting this HMAC computation.
// An explicit output length can be specified; the actual output length will be the minimum of that value and the natural HMAC output length.
// If resultArrayLength is 0, then the natural HMAC output length is selected. The "natural output length" is the output length of the underlying hash function.
br_hmac_init(&hmacContext, &keyContext, resultArrayLength);
// If outputLength is 0, then the natural HMAC output length is selected. The "natural output length" is the output length of the underlying hash function.
br_hmac_init(&hmacContext, &keyContext, outputLength);
// Provide the HMAC context with the data to create a HMAC from.
// The provided dataLength bytes are injected as extra input in the HMAC computation incarnated by the hmacContext.
@ -80,9 +133,8 @@ namespace
return uint8ArrayToHexString(hmac, hmacLength);
}
void *createBearsslHmacCT(const br_hash_class *hashType, const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *createBearsslHmacCT(const br_hash_class *hashType, const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
assert(1 <= resultArrayLength);
assert(_ctMinDataLength <= dataLength && dataLength <= _ctMaxDataLength);
// Comments mainly from https://www.bearssl.org/apidoc/bearssl__hmac_8h.html
@ -100,8 +152,8 @@ namespace
// Initialise a HMAC context with a key context. The key context is unmodified.
// Relevant data from the key context is immediately copied; the key context can thus be independently reused, modified or released without impacting this HMAC computation.
// An explicit output length can be specified; the actual output length will be the minimum of that value and the natural HMAC output length.
// If resultArrayLength is 0, then the natural HMAC output length is selected. The "natural output length" is the output length of the underlying hash function.
br_hmac_init(&hmacContext, &keyContext, resultArrayLength);
// If outputLength is 0, then the natural HMAC output length is selected. The "natural output length" is the output length of the underlying hash function.
br_hmac_init(&hmacContext, &keyContext, outputLength);
// Provide the HMAC context with the data to create a HMAC from.
// The provided dataLength bytes are injected as extra input in the HMAC computation incarnated by the hmacContext.
@ -153,6 +205,9 @@ namespace CryptoInterface
void setWarningsEnabled(bool warningsEnabled) { _warningsEnabled = warningsEnabled; }
bool warningsEnabled() { return _warningsEnabled; }
void setNonceGenerator(nonceGeneratorType nonceGenerator) { _nonceGenerator = nonceGenerator; }
nonceGeneratorType getNonceGenerator() { return _nonceGenerator; }
// #################### MD5 ####################
@ -178,9 +233,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, MD5_NATURAL_LENGTH);
}
void *md5Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *md5Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_md5_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_md5_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String md5Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -188,9 +243,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_md5_vtable, MD5_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *md5HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *md5HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_md5_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_md5_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String md5HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -223,9 +278,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, SHA1_NATURAL_LENGTH);
}
void *sha1Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha1Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_sha1_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_sha1_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha1Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -233,9 +288,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_sha1_vtable, SHA1_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *sha1HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha1HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_sha1_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_sha1_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha1HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -263,9 +318,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, SHA224_NATURAL_LENGTH);
}
void *sha224Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha224Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_sha224_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_sha224_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha224Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -273,9 +328,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_sha224_vtable, SHA224_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *sha224HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha224HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_sha224_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_sha224_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha224HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -303,9 +358,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, SHA256_NATURAL_LENGTH);
}
void *sha256Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha256Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_sha256_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_sha256_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha256Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -313,9 +368,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_sha256_vtable, SHA256_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *sha256HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha256HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_sha256_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_sha256_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha256HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -343,9 +398,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, SHA384_NATURAL_LENGTH);
}
void *sha384Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha384Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_sha384_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_sha384_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha384Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -353,9 +408,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_sha384_vtable, SHA384_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *sha384HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha384HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_sha384_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_sha384_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha384HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -383,9 +438,9 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, SHA512_NATURAL_LENGTH);
}
void *sha512Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha512Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmac(&br_sha512_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmac(&br_sha512_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha512Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -393,9 +448,9 @@ namespace CryptoInterface
return createBearsslHmac(&br_sha512_vtable, SHA512_NATURAL_LENGTH, message, hashKey, hashKeyLength, hmacLength);
}
void *sha512HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength)
void *sha512HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength)
{
return createBearsslHmacCT(&br_sha512_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, resultArrayLength);
return createBearsslHmacCT(&br_sha512_vtable, data, dataLength, hashKey, hashKeyLength, resultArray, outputLength);
}
String sha512HmacCT(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength)
@ -423,4 +478,86 @@ namespace CryptoInterface
return uint8ArrayToHexString(hash, MD5SHA1_NATURAL_LENGTH);
}
// #################### HKDF ####################
void hkdfInit(const void *keyMaterial, const size_t keyMaterialLength, const void *salt, const size_t saltLength)
{
// Comments mainly from https://www.bearssl.org/apidoc/bearssl__kdf_8h.html
br_hkdf_context context;
// Initialize an HKDF context, with a hash function, and the salt. This starts the HKDF-Extract process.
br_hkdf_init(&context, &br_sha256_vtable, salt, saltLength);
// Inject more input bytes. This function may be called repeatedly if the input data is provided by chunks, after br_hkdf_init() but before br_hkdf_flip().
br_hkdf_inject(&context, keyMaterial, keyMaterialLength);
// End the HKDF-Extract process, and start the HKDF-Expand process.
br_hkdf_flip(&context);
_storedHkdfContext = context;
_hkdfContextStored = true;
}
size_t hkdfProduce(void *resultArray, const size_t outputLength, const void *info, const size_t infoLength)
{
// Comments mainly from https://www.bearssl.org/apidoc/bearssl__kdf_8h.html
if(!_hkdfContextStored) // hkdfInit has not yet been executed
return 0;
// HKDF output production (HKDF-Expand).
// Produces more output bytes from the current state. This function may be called several times, but only after br_hkdf_flip().
// Returned value is the number of actually produced bytes. The total output length is limited to 255 times the output length of the underlying hash function.
return br_hkdf_produce(&_storedHkdfContext, info, infoLength, resultArray, outputLength);
}
// #################### Authenticated Encryption with Associated Data (AEAD) ####################
// #################### ChaCha20+Poly1305 AEAD ####################
void chacha20Poly1305Kernel(const int encrypt, void *data, const size_t dataLength, const void *key, const void *keySalt, const size_t keySaltLength,
const void *nonce, void *tag, const void *aad, const size_t aadLength)
{
if(keySalt == nullptr)
{
br_poly1305_ctmul32_run(key, nonce, data, dataLength, aad, aadLength, tag, br_chacha20_ct_run, encrypt);
}
else
{
hkdfInit(key, ENCRYPTION_KEY_LENGTH, keySalt, keySaltLength);
uint8_t derivedEncryptionKey[ENCRYPTION_KEY_LENGTH] {0};
hkdfProduce(derivedEncryptionKey, ENCRYPTION_KEY_LENGTH);
br_poly1305_ctmul32_run(derivedEncryptionKey, nonce, data, dataLength, aad, aadLength, tag, br_chacha20_ct_run, encrypt);
}
}
void chacha20Poly1305Encrypt(void *data, const size_t dataLength, const void *key, const void *keySalt, const size_t keySaltLength,
void *resultingNonce, void *resultingTag, const void *aad, const size_t aadLength)
{
uint8_t *nonce = (uint8_t *)resultingNonce;
getNonceGenerator()(nonce, 12);
chacha20Poly1305Kernel(1, data, dataLength, key, keySalt, keySaltLength, nonce, resultingTag, aad, aadLength);
}
bool chacha20Poly1305Decrypt(void *data, const size_t dataLength, const void *key, const void *keySalt, const size_t keySaltLength,
const void *encryptionNonce, const void *encryptionTag, const void *aad, const size_t aadLength)
{
const uint8_t *oldTag = (const uint8_t *)encryptionTag;
uint8_t newTag[16] {0};
chacha20Poly1305Kernel(0, data, dataLength, key, keySalt, keySaltLength, encryptionNonce, newTag, aad, aadLength);
for(uint32_t i = 0; i < sizeof newTag; ++i)
{
if(newTag[i] != oldTag[i])
return false;
}
return true;
}
}

View File

@ -49,6 +49,12 @@ namespace CryptoInterface
* Using data that exceeds the fixed data length limits will create the wrong HMAC.
*/
/**
* The nonce generator should take an uint8_t array with a given size in bytes and fill it with the nonce.
* The uint8_t array should then be returned by the nonce generator.
*/
using nonceGeneratorType = std::function<uint8_t *(uint8_t *, const size_t)>;
constexpr uint8_t MD5_NATURAL_LENGTH = 16;
constexpr uint8_t SHA1_NATURAL_LENGTH = 20;
@ -63,6 +69,8 @@ namespace CryptoInterface
*/
constexpr uint8_t MD5SHA1_NATURAL_LENGTH = 36;
constexpr uint8_t ENCRYPTION_KEY_LENGTH = 32;
constexpr uint32_t ctMaxDiff = 1073741823; // 2^30 - 1
/**
@ -98,6 +106,14 @@ namespace CryptoInterface
*/
void setWarningsEnabled(bool warningsEnabled);
bool warningsEnabled();
/**
* Set the nonce generator used by the CryptoInterface functions.
*
* @param nonceGenerator The nonce generator to use.
*/
void setNonceGenerator(nonceGeneratorType nonceGenerator);
nonceGeneratorType getNonceGenerator();
// #################### MD5 ####################
@ -125,7 +141,7 @@ namespace CryptoInterface
String md5Hash(const String &message);
/**
* Create a MD5 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a MD5 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -133,12 +149,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than MD5_NATURAL_LENGTH,
* the first (lowest index) MD5_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than MD5_NATURAL_LENGTH,
* the first (lowest index) MD5_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *md5Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *md5Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a MD5 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -154,7 +171,7 @@ namespace CryptoInterface
String md5Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a MD5 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a MD5 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -163,12 +180,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than MD5_NATURAL_LENGTH,
* the first (lowest index) MD5_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than MD5_NATURAL_LENGTH,
* the first (lowest index) MD5_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *md5HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *md5HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a MD5 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -210,7 +228,7 @@ namespace CryptoInterface
String sha1Hash(const String &message);
/**
* Create a SHA1 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA1 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -218,12 +236,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA1_NATURAL_LENGTH,
* the first (lowest index) SHA1_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA1_NATURAL_LENGTH,
* the first (lowest index) SHA1_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha1Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha1Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA1 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -239,7 +258,7 @@ namespace CryptoInterface
String sha1Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a SHA1 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA1 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -248,12 +267,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA1_NATURAL_LENGTH,
* the first (lowest index) SHA1_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA1_NATURAL_LENGTH,
* the first (lowest index) SHA1_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha1HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha1HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA1 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -295,7 +315,7 @@ namespace CryptoInterface
String sha224Hash(const String &message);
/**
* Create a SHA224 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA224 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -303,12 +323,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA224_NATURAL_LENGTH,
* the first (lowest index) SHA224_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA224_NATURAL_LENGTH,
* the first (lowest index) SHA224_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha224Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha224Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA224 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -324,7 +345,7 @@ namespace CryptoInterface
String sha224Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a SHA224 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA224 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -333,12 +354,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA224_NATURAL_LENGTH,
* the first (lowest index) SHA224_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA224_NATURAL_LENGTH,
* the first (lowest index) SHA224_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha224HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha224HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA224 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -380,7 +402,7 @@ namespace CryptoInterface
String sha256Hash(const String &message);
/**
* Create a SHA256 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA256 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -388,12 +410,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA256_NATURAL_LENGTH,
* the first (lowest index) SHA256_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA256_NATURAL_LENGTH,
* the first (lowest index) SHA256_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha256Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha256Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA256 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -409,7 +432,7 @@ namespace CryptoInterface
String sha256Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a SHA256 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA256 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -418,12 +441,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA256_NATURAL_LENGTH,
* the first (lowest index) SHA256_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA256_NATURAL_LENGTH,
* the first (lowest index) SHA256_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha256HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha256HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA256 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -465,7 +489,7 @@ namespace CryptoInterface
String sha384Hash(const String &message);
/**
* Create a SHA384 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA384 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -473,12 +497,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA384_NATURAL_LENGTH,
* the first (lowest index) SHA384_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA384_NATURAL_LENGTH,
* the first (lowest index) SHA384_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha384Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha384Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA384 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -494,7 +519,7 @@ namespace CryptoInterface
String sha384Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a SHA384 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA384 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -503,12 +528,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA384_NATURAL_LENGTH,
* the first (lowest index) SHA384_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA384_NATURAL_LENGTH,
* the first (lowest index) SHA384_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha384HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha384HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA384 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -550,7 +576,7 @@ namespace CryptoInterface
String sha512Hash(const String &message);
/**
* Create a SHA512 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA512 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Uses the BearSSL cryptographic library.
*
* @param data The data array from which to create the HMAC.
@ -558,12 +584,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA512_NATURAL_LENGTH,
* the first (lowest index) SHA512_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA512_NATURAL_LENGTH,
* the first (lowest index) SHA512_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha512Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha512Hmac(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA512 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -579,7 +606,7 @@ namespace CryptoInterface
String sha512Hmac(const String &message, const void *hashKey, const size_t hashKeyLength, const size_t hmacLength);
/**
* Create a SHA512 HMAC from the data, using the provided hashKey. The result will be resultArrayLength bytes long and stored in resultArray.
* Create a SHA512 HMAC from the data, using the provided hashKey. The result will be up to outputLength bytes long and stored in resultArray.
* Constant-time version.
* Uses the BearSSL cryptographic library.
*
@ -588,12 +615,13 @@ namespace CryptoInterface
* @param hashKey The hash key to use when creating the HMAC.
* @param hashKeyLength The length of the hash key in bytes.
* @param resultArray The array wherein to store the resulting HMAC.
* @param resultArrayLength The length of resultArray in bytes. Determines the HMAC length. If resultArrayLength is greater than SHA512_NATURAL_LENGTH,
* the first (lowest index) SHA512_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* @param outputLength The desired length of the generated HMAC, in bytes. Must fit within resultArray. If outputLength is greater than SHA512_NATURAL_LENGTH,
* the first (lowest index) SHA512_NATURAL_LENGTH bytes of resultArray will be used for the HMAC.
* If outputLength is 0, then the natural HMAC output length is selected.
*
* @return A pointer to resultArray.
*/
void *sha512HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t resultArrayLength);
void *sha512HmacCT(const void *data, const size_t dataLength, const void *hashKey, const size_t hashKeyLength, void *resultArray, const size_t outputLength);
/**
* Create a SHA512 HMAC from the message, using the provided hashKey. The result will be hmacLength bytes long and returned as a String in HEX format.
@ -639,6 +667,133 @@ namespace CryptoInterface
* @return A String with the generated hash in HEX format.
*/
String md5sha1Hash(const String &message);
// #################### HKDF ####################
/**
* KDF are functions that takes a variable length input, and provide a variable length output, meant to be used to derive subkeys from a master key.
* HKDF is a KDF defined by RFC 5869. It is based on HMAC. The provided implementation uses SHA-256 as the underlying hash function.
*
* This function initializes the HKDF implementation with the input data to use for HKDF processing.
* Uses the BearSSL cryptographic library.
*
* Must be called at least once before hkdfProduce() can be used.
*
* @param keyMaterial An array containing the key material to use when deriving subkeys. Typically this would be the master key.
* @param keyMaterialLength The length of keyMaterial in bytes.
* @param salt An array containing the salt to use when ingesting key material. Salt is non-secret and can be empty.
* Its role is normally to bind the input to a conventional identifier that qualify it within the used protocol or application.
* @param saltLength The length of the salt array, in bytes.
*/
void hkdfInit(const void *keyMaterial, const size_t keyMaterialLength, const void *salt = nullptr, const size_t saltLength = 0);
/**
* Produce more output bytes from the current HKDF state. This function may be called several times to obtain the full output by chunks.
* The total output size is limited to 255 * SHA256_NATURAL_LENGTH bytes per unique hkdfInit() call.
* Uses the BearSSL cryptographic library.
*
* Should only be used when hkdfInit() has been called at least once.
*
* @param resultArray The array wherein to store the resulting HKDF.
* @param outputLength The requested number of bytes to fill with HKDF output in resultArray.
* @param info NOTE: For correct HKDF processing, the same "info" string must be provided for every call until there's a new unique hkdfInit().
* An array containing the information string to use when producing output. Info is non-secret and can be empty.
* Its role is normally to bind the output to a conventional identifier that qualify it within the used protocol or application.
* @param infoLength The length of the info array, in bytes.
*
* @return The number of HKDF bytes actually produced.
*/
size_t hkdfProduce(void *resultArray, const size_t outputLength, const void *info = nullptr, size_t infoLength = 0);
// #################### Authenticated Encryption with Associated Data (AEAD) ####################
/**
* From https://www.bearssl.org/apidoc/bearssl__aead_8h.html
*
* An AEAD algorithm processes messages and provides confidentiality (encryption) and checked integrity (MAC). It uses the following parameters:
*
* - A symmetric key. Exact size depends on the AEAD algorithm.
* - A nonce (IV). Size depends on the AEAD algorithm; for most algorithms, it is crucial for security that any given nonce value is never used twice for the same key and distinct messages.
* - Data to encrypt and protect.
* - Additional authenticated data, which is covered by the MAC but otherwise left untouched (i.e. not encrypted).
*
* The AEAD algorithm encrypts the data, and produces an authentication tag.
* It is assumed that the encrypted data, the tag, the additional authenticated data and the nonce are sent to the receiver;
* the additional data and the nonce may be implicit (e.g. using elements of the underlying transport protocol, such as record sequence numbers).
* The receiver will recompute the tag value and compare it with the one received;
* if they match, then the data is correct, and can be decrypted and used;
* otherwise, at least one of the elements was altered in transit, normally leading to wholesale rejection of the complete message.
*/
// #################### ChaCha20+Poly1305 AEAD ####################
/**
* Encrypt the data array using the ChaCha20 stream cipher and use Poly1305 for message authentication.
* The function generates in place an equal-length ChaCha20 encrypted version of the data array.
* More information about this encryption standard can be found here: https://tools.ietf.org/html/rfc7539 , https://tools.ietf.org/html/rfc8439
* Uses the BearSSL cryptographic library.
*
* Encryption of small messages (up to a few hundred data bytes) takes around 0.5-1 ms with the default nonceGenerator, half of this without keySalt.
*
* The output values of chacha20Poly1305Encrypt should be passed as input values to chacha20Poly1305Decrypt.
*
* Note that a 12 byte nonce is generated via getNonceGenerator() every time chacha20Poly1305Encrypt is called.
* If the same key and nonce combination is used more than once for distinct messages, the encryption will be broken, so keep the following in mind:
*
* By default the nonce is generated via the hardware random number generator of the ESP8266.
* The entropy of this source may not be sufficient to avoid nonce collisions, so to further reduce the risk of encryption failure
* it is recommended that a keySalt is always provided when using the default nonceGenerator. Using a keySalt will create a
* pseudorandom subkey from the original key via HKDF, and use that for the encryption/decryption.
* The same key + keySalt will always generate the same subkey.
*
* An alternative to using a keySalt is to change the nonceGenerator so that it does not rely on random numbers.
* One way to do this would be to use a counter that guarantees the same key + nonce combination is never used.
* This may not be easily achievable in all scenarios, however.
*
* @param data An array containing the data to encrypt. The encrypted data is generated in place, so when the function returns the data array will contain the encrypted data.
* @param dataLength The length of the data array in bytes.
* @param key The secret encryption key to use.
* @param keySalt The salt to use when generating a subkey from key. Note that the same salt must be used during decryption as during encryption. Set to nullptr to prevent subkey generation.
* @param keySaltLength The length of keySalt in bytes.
* @param resultingNonce The array that will store the nonce generated during encryption. Must be able to contain at least 12 bytes. The nonce is not secret and must be passed to the decryption function.
* @param resultingTag The array that will store the message authentication tag generated during encryption. Must be able to contain at least 16 bytes. The tag is not secret and must be passed to the decryption function.
* @param aad Additional authenticated data. This data will be covered by the Poly1305 MAC, but not encrypted.
* You can include the unencrypted parts of your message as AAD to ensure that the encrypted content cannot
* be re-sent with replaced unencrypted data by an attacker.
* Defaults to nullptr.
* @param aadLength The length of the aad array in bytes. Defaults to 0.
*/
void chacha20Poly1305Encrypt(void *data, const size_t dataLength, const void *key, const void *keySalt, const size_t keySaltLength, void *resultingNonce, void *resultingTag, const void *aad = nullptr, const size_t aadLength = 0);
/**
* Decrypt the data array using the ChaCha20 stream cipher and use Poly1305 for message authentication.
* The function generates in place an equal-length ChaCha20 decrypted version of the data array.
* More information about this encryption standard can be found here: https://tools.ietf.org/html/rfc7539 , https://tools.ietf.org/html/rfc8439
* Uses the BearSSL cryptographic library.
*
* Decryption of small messages (up to a few hundred data bytes) takes around 0.5-1 ms, half of this without keySalt.
*
* The output values of chacha20Poly1305Encrypt should be passed as input values to chacha20Poly1305Decrypt.
*
* @param data An array containing the data to decrypt. The decrypted data is generated in place, so when the function returns the data array will contain the decrypted data.
* @param dataLength The length of the data array in bytes.
* @param key The secret encryption key to use.
* @param keySalt The salt to use when generating a subkey from key. Note that the same salt must be used during decryption as during encryption. Set to nullptr to prevent subkey generation.
* @param keySaltLength The length of keySalt in bytes.
* @param encryptionNonce An array containing the nonce that was generated during encryption. The nonce should be 12 bytes.
* @param encryptionTag An array containing the message authentication tag that was generated during encryption. The tag should be 16 bytes.
* @param aad Additional authenticated data. This data will be covered by the Poly1305 MAC, but not decrypted.
* You can include the unencrypted parts of your message as AAD to ensure that the encrypted content cannot
* be re-sent with replaced unencrypted data by an attacker.
* Defaults to nullptr.
* @param aadLength The length of the aad array in bytes. Defaults to 0.
*
* @return True if the decryption was successful (the generated tag matches encryptionTag). False otherwise. Note that the data array is modified regardless of this outcome.
*/
bool chacha20Poly1305Decrypt(void *data, const size_t dataLength, const void *key, const void *keySalt, const size_t keySaltLength, const void *encryptionNonce, const void *encryptionTag, const void *aad = nullptr, const size_t aadLength = 0);
}
#endif

View File

@ -128,18 +128,21 @@ uint64_t EncryptedConnectionData::getOwnSessionKey() const { return _ownSessionK
uint64_t EncryptedConnectionData::incrementSessionKey(uint64_t sessionKey, const uint8_t *hashKey, uint8_t hashKeyLength)
{
String hmac = MeshCryptoInterface::createMeshHmac(uint64ToString(sessionKey), hashKey, hashKeyLength);
uint8_t inputArray[8] {0};
uint8_t hmacArray[CryptoInterface::SHA256_NATURAL_LENGTH] {0};
CryptoInterface::sha256Hmac(uint64ToUint8Array(sessionKey, inputArray), 8, hashKey, hashKeyLength, hmacArray, CryptoInterface::SHA256_NATURAL_LENGTH);
/* HMAC truncation should be OK since hmac sha256 is a PRF and we are truncating to the leftmost (MSB) bits.
PRF: https://crypto.stackexchange.com/questions/26410/whats-the-gcm-sha-256-of-a-tls-protocol/26434#26434
Truncate to leftmost bits: https://tools.ietf.org/html/rfc2104#section-5 */
uint64_t newLeftmostBits = strtoul(hmac.substring(0, 8).c_str(), nullptr, HEX); // strtoul stops reading input when an invalid character is discovered.
uint64_t newLeftmostBits = uint8ArrayToUint64(hmacArray) & EspnowProtocolInterpreter::uint64LeftmostBits;
if(newLeftmostBits == 0)
newLeftmostBits = RANDOM_REG32 | (1 << 31); // We never want newLeftmostBits == 0 since that would indicate an unencrypted transmission.
newLeftmostBits = ((uint64_t)RANDOM_REG32 | (1 << 31)) << 32; // We never want newLeftmostBits == 0 since that would indicate an unencrypted transmission.
uint64_t newRightmostBits = (uint32_t)(sessionKey + 1);
return (newLeftmostBits << 32) | newRightmostBits;
return newLeftmostBits | newRightmostBits;
}
void EncryptedConnectionData::incrementOwnSessionKey()

View File

@ -76,7 +76,7 @@ public:
void setDesync(bool desync);
bool desync() const;
// Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized.
// Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey are not serialized.
// These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection.
String serialize() const;

View File

@ -26,8 +26,9 @@ extern "C" {
#include "UtilityFunctions.h"
#include "MutexTracker.h"
#include "JsonTranslator.h"
#include "MeshCryptoInterface.h"
using EspnowProtocolInterpreter::espnowEncryptionKeyLength;
using EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength;
using EspnowProtocolInterpreter::espnowHashKeyLength;
static const uint8_t maxEncryptedConnections = 6; // This is limited by the ESP-NOW API. Max 6 in AP or AP+STA mode. Max 10 in STA mode. See "ESP-NOW User Guide" for more info.
@ -38,6 +39,7 @@ const uint8_t EspnowMeshBackend::broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF
bool EspnowMeshBackend::_espnowTransmissionMutex = false;
bool EspnowMeshBackend::_espnowConnectionQueueMutex = false;
bool EspnowMeshBackend::_responsesToSendMutex = false;
EspnowMeshBackend *EspnowMeshBackend::_espnowRequestManager = nullptr;
@ -67,10 +69,13 @@ encrypted_connection_status_t EspnowMeshBackend::_ongoingPeerRequestResult = ECS
uint32_t EspnowMeshBackend::_ongoingPeerRequestEncryptionStart = 0;
bool EspnowMeshBackend::_reciprocalPeerRequestConfirmation = false;
uint8_t EspnowMeshBackend::_espnowEncryptionKok[espnowEncryptionKeyLength] = { 0 };
uint8_t EspnowMeshBackend::_espnowEncryptionKok[espnowEncryptedConnectionKeyLength] = { 0 };
bool EspnowMeshBackend::_espnowEncryptionKokSet = false;
uint32_t EspnowMeshBackend::_unencryptedMessageID = 0;
uint8_t EspnowMeshBackend::_espnowMessageEncryptionKey[CryptoInterface::ENCRYPTION_KEY_LENGTH] = { 0 };
bool EspnowMeshBackend::_useEncryptedMessages = false;
uint32_t EspnowMeshBackend::_unsynchronizedMessageID = 0;
// _logEntryLifetimeMs is based on someone storing 40 responses of 750 bytes each = 30 000 bytes (roughly full memory),
// which takes 2000 ms + some margin to send. Also, we want to avoid old entries taking up memory if they cannot be sent,
@ -103,8 +108,7 @@ void espnowDelay(uint32_t durationMs)
}
EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter,
broadcastFilterType broadcastFilter, const String &meshPassword, const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength],
const uint8_t espnowHashKey[espnowHashKeyLength], const String &ssidPrefix, const String &ssidSuffix, bool verboseMode,
broadcastFilterType broadcastFilter, const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, bool verboseMode,
uint8 meshWiFiChannel)
: MeshBackendBase(requestHandler, responseHandler, networkFilter, MB_ESP_NOW)
{
@ -114,12 +118,30 @@ EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, response
setBroadcastFilter(broadcastFilter);
setSSID(ssidPrefix, "", ssidSuffix);
setMeshPassword(meshPassword);
setEspnowEncryptionKey(espnowEncryptionKey);
setEspnowHashKey(espnowHashKey);
setVerboseModeState(verboseMode);
setWiFiChannel(meshWiFiChannel);
}
EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter,
broadcastFilterType broadcastFilter, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[espnowHashKeyLength], const String &ssidPrefix, const String &ssidSuffix, bool verboseMode,
uint8 meshWiFiChannel)
: EspnowMeshBackend(requestHandler, responseHandler, networkFilter, broadcastFilter, meshPassword, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKey);
setEspnowHashKey(espnowHashKey);
}
EspnowMeshBackend::EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter,
broadcastFilterType broadcastFilter, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed,
const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, bool verboseMode,
uint8 meshWiFiChannel)
: EspnowMeshBackend(requestHandler, responseHandler, networkFilter, broadcastFilter, meshPassword, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed);
setEspnowHashKey(espnowHashKeySeed);
}
EspnowMeshBackend::~EspnowMeshBackend()
{
if(isEspnowRequestManager())
@ -142,7 +164,7 @@ bool EspnowMeshBackend::activateEspnow()
{
if (esp_now_init()==0)
{
if(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok returns 0 on success.
if(_espnowEncryptionKokSet && esp_now_set_kok(_espnowEncryptionKok, espnowEncryptedConnectionKeyLength)) // esp_now_set_kok returns 0 on success.
warningPrint("Failed to set ESP-NOW KoK!");
if(getEspnowRequestManager() == nullptr)
@ -388,7 +410,7 @@ void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *
// Otherwise we get issues such as _espnowTransmissionMutex will usually be free, but occasionally taken (when callback occurs in a delay() during attemptTransmission).
MutexTracker captureBanTracker(MutexTracker::captureBan());
if(len >= EspnowProtocolInterpreter::espnowProtocolBytesSize()) // If we do not receive at least the protocol bytes, the transmission is invalid.
if(len >= espnowMetadataSize()) // If we do not receive at least the metadata bytes, the transmission is invalid.
{
//uint32_t callbackStart = millis();
@ -398,11 +420,21 @@ void EspnowMeshBackend::espnowReceiveCallbackWrapper(uint8_t *macaddr, uint8_t *
char messageType = espnowGetMessageType(dataArray);
uint64_t receivedMessageID = espnowGetMessageID(dataArray);
if(currentEspnowRequestManager && !currentEspnowRequestManager->acceptsUnencryptedRequests()
if(currentEspnowRequestManager && !currentEspnowRequestManager->acceptsUnverifiedRequests()
&& !usesConstantSessionKey(messageType) && !verifyPeerSessionKey(receivedMessageID, macaddr, messageType))
{
return;
}
if(useEncryptedMessages())
{
// chacha20Poly1305Decrypt decrypts dataArray in place.
if(!CryptoInterface::chacha20Poly1305Decrypt(dataArray + espnowMetadataSize(), len - espnowMetadataSize(), getEspnowMessageEncryptionKey(), dataArray,
espnowProtocolBytesSize, dataArray + espnowProtocolBytesSize, dataArray + espnowProtocolBytesSize + 12))
{
return; // Decryption of message failed.
}
}
uint64_t uint64StationMac = macToUint64(macaddr);
bool transmissionEncrypted = usesEncryption(receivedMessageID);
@ -759,7 +791,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
String message = espnowGetMessageContent(dataArray, len);
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
setReceivedEncryptedTransmission(usesEncryption(messageID));
bool acceptBroadcast = getBroadcastFilter()(message, *this);
if(acceptBroadcast)
{
@ -815,7 +847,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
// Copy totalMessage in case user callbacks (request/responseHandler) do something odd with receivedEspnowTransmissions list.
String totalMessage = storedMessageIterator->second.getTotalMessage(); // https://stackoverflow.com/questions/134731/returning-a-const-reference-to-an-object-instead-of-a-copy It is likely that most compilers will perform Named Value Return Value Optimisation in this case
receivedEspnowTransmissions.erase(storedMessageIterator); // Erase the extra copy of the totalMessage, to save RAM.
//Serial.println("methodStart erase done " + String(millis() - methodStart));
@ -827,7 +859,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
setReceivedEncryptedTransmission(usesEncryption(messageID));
String response = getRequestHandler()(totalMessage, *this);
//Serial.println("methodStart response acquired " + String(millis() - methodStart));
@ -853,7 +885,7 @@ void EspnowMeshBackend::espnowReceiveCallback(uint8_t *macaddr, uint8_t *dataArr
setSenderMac(macaddr);
espnowGetTransmissionMac(dataArray, _senderAPMac);
setReceivedEncryptedMessage(usesEncryption(messageID));
setReceivedEncryptedTransmission(usesEncryption(messageID));
getResponseHandler()(totalMessage, *this);
}
else
@ -979,7 +1011,7 @@ uint64_t EspnowMeshBackend::generateMessageID(EncryptedConnectionLog *encryptedC
return encryptedConnection->getOwnSessionKey();
}
return _unencryptedMessageID++;
return _unsynchronizedMessageID++;
}
uint64_t EspnowMeshBackend::createSessionKey()
@ -1073,7 +1105,7 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St
int32_t transmissionsRemaining = transmissionsRequired > 1 ? transmissionsRequired - 1 : 0;
_transmissionsTotal++;
// Though it is possible to handle messages requiring more than 3 transmissions with the current design, transmission fail rates would increase dramatically.
// Messages composed of up to 128 transmissions can be handled without modification, but RAM limitations on the ESP8266 would make this hard in practice.
// We thus prefer to keep the code simple and performant instead.
@ -1087,7 +1119,7 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St
uint8_t transmissionSize = 0;
bool messageStart = true;
uint8_t sizeOfProtocolBytes = espnowProtocolBytesSize();
uint8_t metadataSize = espnowMetadataSize();
do
{
@ -1105,12 +1137,19 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St
////// Create transmission array //////
if(transmissionsRemaining > 0)
{
transmissionSize = getMaxBytesPerTransmission();
else if(message.length() == 0)
transmissionSize = sizeOfProtocolBytes;
}
else
transmissionSize = sizeOfProtocolBytes + (message.length() % getMaxMessageBytesPerTransmission() == 0 ?
getMaxMessageBytesPerTransmission() : message.length() % getMaxMessageBytesPerTransmission());
{
transmissionSize = metadataSize;
if(message.length() > 0)
{
uint32_t remainingLength = message.length() % getMaxMessageBytesPerTransmission();
transmissionSize += (remainingLength == 0 ? getMaxMessageBytesPerTransmission() : remainingLength);
}
}
uint8_t transmission[transmissionSize];
@ -1136,9 +1175,17 @@ transmission_status_t EspnowMeshBackend::espnowSendToNodeUnsynchronized(const St
////// Fill message bytes //////
int32_t transmissionStartIndex = (transmissionsRequired - transmissionsRemaining - 1) * getMaxMessageBytesPerTransmission();
std::copy_n(message.substring(transmissionStartIndex, transmissionStartIndex + transmissionSize - sizeOfProtocolBytes).c_str(),
transmissionSize - sizeOfProtocolBytes, transmission + sizeOfProtocolBytes);
std::copy_n(message.begin() + transmissionStartIndex, transmissionSize - metadataSize, transmission + metadataSize);
if(useEncryptedMessages())
{
// chacha20Poly1305Encrypt encrypts transmission in place.
// We are using the protocol bytes as a key salt.
CryptoInterface::chacha20Poly1305Encrypt(transmission + metadataSize, transmissionSize - metadataSize, getEspnowMessageEncryptionKey(), transmission,
espnowProtocolBytesSize, transmission + espnowProtocolBytesSize, transmission + espnowProtocolBytesSize + 12);
}
////// Transmit //////
uint32_t retransmissions = 0;
@ -1238,33 +1285,38 @@ uint64_t EspnowMeshBackend::macAndTypeToUint64Mac(const macAndType_td &macAndTyp
return static_cast<uint64_t>(macAndTypeValue) >> 8;
}
void EspnowMeshBackend::setEspnowEncryptionKey(const uint8_t espnowEncryptionKey[espnowEncryptionKeyLength])
void EspnowMeshBackend::setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[espnowEncryptedConnectionKeyLength])
{
assert(espnowEncryptionKey != nullptr);
assert(espnowEncryptedConnectionKey != nullptr);
for(int i = 0; i < espnowEncryptionKeyLength; i++)
for(int i = 0; i < espnowEncryptedConnectionKeyLength; i++)
{
_espnowEncryptionKey[i] = espnowEncryptionKey[i];
_espnowEncryptedConnectionKey[i] = espnowEncryptedConnectionKey[i];
}
}
const uint8_t *EspnowMeshBackend::getEspnowEncryptionKey()
void EspnowMeshBackend::setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed)
{
return _espnowEncryptionKey;
MeshCryptoInterface::initializeKey(_espnowEncryptedConnectionKey, espnowEncryptedConnectionKeyLength, espnowEncryptedConnectionKeySeed);
}
uint8_t *EspnowMeshBackend::getEspnowEncryptionKey(uint8_t resultArray[espnowEncryptionKeyLength])
const uint8_t *EspnowMeshBackend::getEspnowEncryptedConnectionKey()
{
std::copy_n(_espnowEncryptionKey, espnowEncryptionKeyLength, resultArray);
return _espnowEncryptedConnectionKey;
}
uint8_t *EspnowMeshBackend::getEspnowEncryptedConnectionKey(uint8_t resultArray[espnowEncryptedConnectionKeyLength])
{
std::copy_n(_espnowEncryptedConnectionKey, espnowEncryptedConnectionKeyLength, resultArray);
return resultArray;
}
bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[espnowEncryptionKeyLength])
bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[espnowEncryptedConnectionKeyLength])
{
if(espnowEncryptionKok == nullptr || esp_now_set_kok(espnowEncryptionKok, espnowEncryptionKeyLength)) // esp_now_set_kok failed if not == 0
if(espnowEncryptionKok == nullptr || esp_now_set_kok(espnowEncryptionKok, espnowEncryptedConnectionKeyLength)) // esp_now_set_kok failed if not == 0
return false;
for(int i = 0; i < espnowEncryptionKeyLength; i++)
for(int i = 0; i < espnowEncryptedConnectionKeyLength; i++)
{
_espnowEncryptionKok[i] = espnowEncryptionKok[i];
}
@ -1274,6 +1326,14 @@ bool EspnowMeshBackend::setEspnowEncryptionKok(uint8_t espnowEncryptionKok[espno
return true;
}
bool EspnowMeshBackend::setEspnowEncryptionKok(const String &espnowEncryptionKokSeed)
{
uint8_t espnowEncryptionKok[espnowEncryptedConnectionKeyLength] {};
MeshCryptoInterface::initializeKey(espnowEncryptionKok, espnowEncryptedConnectionKeyLength, espnowEncryptionKokSeed);
return setEspnowEncryptionKok(espnowEncryptionKok);
}
const uint8_t *EspnowMeshBackend::getEspnowEncryptionKok()
{
if(_espnowEncryptionKokSet)
@ -1292,11 +1352,48 @@ void EspnowMeshBackend::setEspnowHashKey(const uint8_t espnowHashKey[espnowHashK
}
}
void EspnowMeshBackend::setEspnowHashKey(const String &espnowHashKeySeed)
{
MeshCryptoInterface::initializeKey(_espnowHashKey, espnowHashKeyLength, espnowHashKeySeed);
}
const uint8_t *EspnowMeshBackend::getEspnowHashKey()
{
return _espnowHashKey;
}
void EspnowMeshBackend::setEspnowMessageEncryptionKey(uint8_t espnowMessageEncryptionKey[CryptoInterface::ENCRYPTION_KEY_LENGTH])
{
assert(espnowMessageEncryptionKey != nullptr);
for(int i = 0; i < CryptoInterface::ENCRYPTION_KEY_LENGTH; i++)
{
_espnowMessageEncryptionKey[i] = espnowMessageEncryptionKey[i];
}
}
void EspnowMeshBackend::setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed)
{
MeshCryptoInterface::initializeKey(_espnowMessageEncryptionKey, CryptoInterface::ENCRYPTION_KEY_LENGTH, espnowMessageEncryptionKeySeed);
}
const uint8_t *EspnowMeshBackend::getEspnowMessageEncryptionKey()
{
return _espnowMessageEncryptionKey;
}
void EspnowMeshBackend::setUseEncryptedMessages(bool useEncryptedMessages)
{
MutexTracker mutexTracker(_espnowSendToNodeMutex);
if(!mutexTracker.mutexCaptured())
{
assert(false && "ERROR! espnowSendToNode in progress. Don't call setUseEncryptedMessages from non-hook callbacks since this may modify the ESP-NOW transmission parameters during ongoing transmissions! Aborting.");
}
_useEncryptedMessages = useEncryptedMessages;
}
bool EspnowMeshBackend::useEncryptedMessages() { return _useEncryptedMessages; }
bool EspnowMeshBackend::verifyPeerSessionKey(uint64_t sessionKey, const uint8_t *peerMac, char messageType)
{
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerMac))
@ -1375,16 +1472,27 @@ const uint8_t *EspnowMeshBackend::getScheduledResponseRecipient(uint32_t respons
return getScheduledResponse(responseIndex)->getRecipientMac();
}
uint32_t EspnowMeshBackend::numberOfScheduledResponses() {return responsesToSend.size();}
void EspnowMeshBackend::clearAllScheduledResponses()
{
MutexTracker responsesToSendMutexTracker(_responsesToSendMutex);
if(!responsesToSendMutexTracker.mutexCaptured())
{
assert(false && "ERROR! responsesToSend locked. Don't call clearAllScheduledResponses from callbacks as this may corrupt program state! Aborting.");
}
responsesToSend.clear();
}
uint32_t EspnowMeshBackend::numberOfScheduledResponses() {return responsesToSend.size();}
void EspnowMeshBackend::deleteScheduledResponsesByRecipient(const uint8_t *recipientMac, bool encryptedOnly)
{
MutexTracker responsesToSendMutexTracker(_responsesToSendMutex);
if(!responsesToSendMutexTracker.mutexCaptured())
{
assert(false && "ERROR! responsesToSend locked. Don't call deleteScheduledResponsesByRecipient from callbacks as this may corrupt program state! Aborting.");
}
for(auto responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); )
{
if(macEqual(responseIterator->getRecipientMac(), recipientMac) && (!encryptedOnly || EspnowProtocolInterpreter::usesEncryption(responseIterator->getRequestID())))
@ -1420,19 +1528,19 @@ uint8_t *EspnowMeshBackend::getSenderAPMac(uint8_t *macArray)
return macArray;
}
void EspnowMeshBackend::setReceivedEncryptedMessage(bool receivedEncryptedMessage) { _receivedEncryptedMessage = receivedEncryptedMessage; }
bool EspnowMeshBackend::receivedEncryptedMessage() {return _receivedEncryptedMessage;}
void EspnowMeshBackend::setReceivedEncryptedTransmission(bool receivedEncryptedTransmission) { _receivedEncryptedTransmission = receivedEncryptedTransmission; }
bool EspnowMeshBackend::receivedEncryptedTransmission() {return _receivedEncryptedTransmission;}
bool EspnowMeshBackend::addUnencryptedConnection(const String &serializedConnectionState)
{
return JsonTranslator::getUnencryptedMessageID(serializedConnectionState, _unencryptedMessageID);
return JsonTranslator::getUnsynchronizedMessageID(serializedConnectionState, _unsynchronizedMessageID);
}
encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey)
{
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 };
uint8_t encryptionKeyArray[espnowEncryptedConnectionKeyLength] = { 0 };
if(EncryptedConnectionLog *encryptedConnection = getEncryptedConnection(peerStaMac))
{
@ -1440,7 +1548,7 @@ encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(uint8_t
temporaryEncryptedConnectionToPermanent(peerStaMac);
encryptedConnection->setPeerSessionKey(peerSessionKey);
encryptedConnection->setOwnSessionKey(ownSessionKey);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptedConnectionKey(encryptionKeyArray), espnowEncryptedConnectionKeyLength);
encryptedConnection->setHashKey(getEspnowHashKey());
return ECS_CONNECTION_ESTABLISHED;
@ -1453,7 +1561,7 @@ encrypted_connection_status_t EspnowMeshBackend::addEncryptedConnection(uint8_t
}
// returns 0 on success: int esp_now_add_peer(u8 *mac_addr, u8 role, u8 channel, u8 *key, u8 key_len)
// Only MAC, encryption key and key length (16) actually matter. The rest is not used by ESP-NOW.
else if(0 == esp_now_add_peer(peerStaMac, ESP_NOW_ROLE_CONTROLLER, getWiFiChannel(), getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength))
else if(0 == esp_now_add_peer(peerStaMac, ESP_NOW_ROLE_CONTROLLER, getWiFiChannel(), getEspnowEncryptedConnectionKey(encryptionKeyArray), espnowEncryptedConnectionKeyLength))
{
encryptedConnections.emplace_back(peerStaMac, peerApMac, peerSessionKey, ownSessionKey, getEspnowHashKey());
return ECS_CONNECTION_ESTABLISHED;
@ -1506,7 +1614,7 @@ encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection
{
assert(encryptedConnections.size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library
uint8_t encryptionKeyArray[espnowEncryptionKeyLength] = { 0 };
uint8_t encryptionKeyArray[espnowEncryptedConnectionKeyLength] = { 0 };
connectionLogIterator encryptedConnection = connectionLogEndIterator();
@ -1515,7 +1623,7 @@ encrypted_connection_status_t EspnowMeshBackend::addTemporaryEncryptedConnection
// There is already an encrypted connection to this mac, so no need to replace it, just updating is enough.
encryptedConnection->setPeerSessionKey(peerSessionKey);
encryptedConnection->setOwnSessionKey(ownSessionKey);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptionKey(encryptionKeyArray), espnowEncryptionKeyLength);
esp_now_set_peer_key(peerStaMac, getEspnowEncryptedConnectionKey(encryptionKeyArray), espnowEncryptedConnectionKeyLength);
encryptedConnection->setHashKey(getEspnowHashKey());
if(encryptedConnection->temporary())
@ -1935,8 +2043,8 @@ encrypted_connection_removal_outcome_t EspnowMeshBackend::requestEncryptedConnec
}
}
void EspnowMeshBackend::setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests) { _acceptsUnencryptedRequests = acceptsUnencryptedRequests; }
bool EspnowMeshBackend::acceptsUnencryptedRequests() { return _acceptsUnencryptedRequests; }
void EspnowMeshBackend::setAcceptsUnverifiedRequests(bool acceptsUnverifiedRequests) { _acceptsUnverifiedRequests = acceptsUnverifiedRequests; }
bool EspnowMeshBackend::acceptsUnverifiedRequests() { return _acceptsUnverifiedRequests; }
void EspnowMeshBackend::setEncryptedConnectionsSoftLimit(uint8_t softLimit)
{
@ -2269,6 +2377,9 @@ void EspnowMeshBackend::broadcast(const String &message)
void EspnowMeshBackend::setBroadcastTransmissionRedundancy(uint8_t redundancy) { _broadcastTransmissionRedundancy = redundancy; }
uint8_t EspnowMeshBackend::getBroadcastTransmissionRedundancy() { return _broadcastTransmissionRedundancy; }
void EspnowMeshBackend::setResponseTransmittedHook(responseTransmittedHookType responseTransmittedHook) { _responseTransmittedHook = responseTransmittedHook; }
EspnowMeshBackend::responseTransmittedHookType EspnowMeshBackend::getResponseTransmittedHook() { return _responseTransmittedHook; }
void EspnowMeshBackend::sendStoredEspnowMessages(const ExpiringTimeTracker *estimatedMaxDurationTracker)
{
sendPeerRequestConfirmations(estimatedMaxDurationTracker);
@ -2320,8 +2431,6 @@ void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *
// Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode
// (which may add an element to the peerRequestConfirmationsToSend list).
staticVerboseModePrint("Responding to encrypted connection request from MAC " + macToString(defaultBSSID));
if(!existingEncryptedConnection &&
((reciprocalPeerRequest && encryptedConnections.size() >= maxEncryptedConnections) || (!reciprocalPeerRequest && reservedEncryptedConnections() >= maxEncryptedConnections)))
{
@ -2421,8 +2530,15 @@ void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *
void EspnowMeshBackend::sendEspnowResponses(const ExpiringTimeTracker *estimatedMaxDurationTracker)
{
uint32_t bufferedCriticalHeapLevel = criticalHeapLevel() + criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical.
for(std::list<ResponseData>::iterator responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); )
MutexTracker responsesToSendMutexTracker(_responsesToSendMutex);
if(!responsesToSendMutexTracker.mutexCaptured())
{
assert(false && "ERROR! responsesToSend locked. Don't call sendEspnowResponses from callbacks as this may corrupt program state! Aborting.");
}
uint32_t responseIndex = 0;
for(std::list<ResponseData>::iterator responseIterator = responsesToSend.begin(); responseIterator != responsesToSend.end(); ++responseIndex)
{
if(responseIterator->timeSinceCreation() > logEntryLifetimeMs())
{
@ -2433,12 +2549,17 @@ void EspnowMeshBackend::sendEspnowResponses(const ExpiringTimeTracker *estimated
continue;
}
bool hookOutcome = true;
// Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode
// (which may add an element to the responsesToSend list).
if(espnowSendToNodeUnsynchronized(responseIterator->getMessage(), responseIterator->getRecipientMac(), 'A', responseIterator->getRequestID())
== TS_TRANSMISSION_COMPLETE)
{
{
if(EspnowMeshBackend *currentEspnowRequestManager = getEspnowRequestManager())
hookOutcome = currentEspnowRequestManager->getResponseTransmittedHook()(responseIterator->getMessage(), responseIterator->getRecipientMac(), responseIndex, *currentEspnowRequestManager);
responseIterator = responsesToSend.erase(responseIterator);
--responseIndex;
}
else
{
@ -2454,7 +2575,7 @@ void EspnowMeshBackend::sendEspnowResponses(const ExpiringTimeTracker *estimated
return; // responseIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation.
}
if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired())
if(!hookOutcome || (estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()))
return;
}
}
@ -2466,7 +2587,8 @@ uint32_t EspnowMeshBackend::getMaxBytesPerTransmission()
uint32_t EspnowMeshBackend::getMaxMessageBytesPerTransmission()
{
return getMaxBytesPerTransmission() - EspnowProtocolInterpreter::espnowProtocolBytesSize();
using namespace EspnowProtocolInterpreter;
return getMaxBytesPerTransmission() - espnowMetadataSize();
}
void EspnowMeshBackend::setMaxTransmissionsPerMessage(uint8_t maxTransmissionsPerMessage)
@ -2560,9 +2682,9 @@ String EspnowMeshBackend::serializeUnencryptedConnection()
{
using namespace JsonTranslator;
// Returns: {"connectionState":{"unencMsgID":"123"}}
// Returns: {"connectionState":{"unsyncMsgID":"123"}}
return jsonConnectionState + createJsonEndPair(jsonUnencryptedMessageID, String(_unencryptedMessageID));
return jsonConnectionState + createJsonEndPair(jsonUnsynchronizedMessageID, String(_unsynchronizedMessageID));
}
String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac)

View File

@ -42,7 +42,7 @@
* This enables flexible easy-to-use encrypted ESP-NOW communication. 'P' and 'C' messages can be encrypted.
* The encryption pairing process works as follows (from top to bottom):
*
* Encryption pairing process, schematic overview:
* Encrypted connection pairing process, schematic overview:
*
* Connection | Peer sends ('C'): | Peer requester sends ('P'): | Connection
* encrypted: | | | encrypted:
@ -66,6 +66,13 @@
* Messages of type 'A' and 'C' are response types, and thus use the same session key as the corresponding 'R' and 'P' message they are responding to.
* This means they can never cause a desynchronization to occur, and therefore they do not trigger 'S' messages.
*
* In addition to using encrypted ESP-NOW connections the framework can also send automatically encrypted messages (AEAD) over both encrypted and unencrypted connections.
* Using AEAD will only encrypt the message content, not the transmission metadata.
* The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer.
* AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections.
* Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption,
* and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission.
* Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established.
*/
#ifndef __ESPNOWMESHBACKEND_H__
@ -81,6 +88,7 @@
#include <map>
#include <list>
#include "EspnowNetworkInfo.h"
#include "CryptoInterface.h"
typedef enum
{
@ -127,6 +135,7 @@ class EspnowMeshBackend : public MeshBackendBase {
protected:
typedef std::function<bool(String &, EspnowMeshBackend &)> broadcastFilterType;
typedef std::function<bool(const String &, const uint8_t *, uint32_t, EspnowMeshBackend &)> responseTransmittedHookType;
public:
@ -140,7 +149,7 @@ public:
* @param networkFilter The callback handler for deciding which WiFi networks to connect to.
* @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptionKey An uint8_t array containing the key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowEncryptedConnectionKey An uint8_t array containing the secret key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKey An uint8_t array containing the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
@ -154,10 +163,37 @@ public:
*
*/
EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, broadcastFilterType broadcastFilter,
const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
/**
* ESP-NOW constructor method. Creates an ESP-NOW node, ready to be initialised.
*
* @param requestHandler The callback handler for dealing with received requests. Takes a string as an argument which
* is the request string received from another node and returns the string to send back.
* @param responseHandler The callback handler for dealing with received responses. Takes a string as an argument which
* is the response string received from another node. Returns a transmission status code as a transmission_status_t.
* @param networkFilter The callback handler for deciding which WiFi networks to connect to.
* @param broadcastFilter The callback handler for deciding which ESP-NOW broadcasts to accept.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptedConnectionKeySeed A string containing the seed that will generate the secret key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKeySeed A string containing the seed that will generate the secret key used by this EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
* @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances.
* @param meshWiFiChannel The WiFi channel used by the mesh network. Valid values are integers from 1 to 13. Defaults to 1.
* WARNING: The ESP8266 has only one WiFi channel, and the the station/client mode is always prioritized for channel selection.
* This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels.
* In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the
* WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly
* make it impossible for other stations to detect the APs whose WiFi channels have changed.
*
*/
EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, broadcastFilterType broadcastFilter,
const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
~EspnowMeshBackend() override;
/**
@ -327,20 +363,50 @@ public:
*
* NOTE: Encrypted connections added before the encryption key change will retain their old encryption key.
* Only changes the encryption key used by this EspnowMeshBackend instance, so each instance can use a separate key.
* Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible.
* Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
* Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
*
* @param espnowEncryptionKey An array containing the espnowEncryptionKeyLength bytes that will be used as the encryption key.
* @param espnowEncryptedConnectionKey An array containing the espnowEncryptedConnectionKeyLength bytes that will be used as the encryption key.
*/
void setEspnowEncryptionKey(const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength]);
void setEspnowEncryptedConnectionKey(const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength]);
/**
* Change the key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* Will apply to any new received requests for encrypted connection if this EspnowMeshBackend instance is the current request manager.
* Will apply to any new encrypted connections requested or added by this EspnowMeshBackend instance.
*
* NOTE: Encrypted connections added before the encryption key change will retain their old encryption key.
* Only changes the encryption key used by this EspnowMeshBackend instance, so each instance can use a separate key.
* Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
* Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
*
* @param espnowHashKeySeed A string that will be used to generate the encryption key. The same string will always generate the same key.
* A minimum of 8 random characters are recommended to ensure sufficient key variation.
*/
void setEspnowEncryptedConnectionKey(const String &espnowEncryptedConnectionKeySeed);
/**
* Get the encryption key used by this EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
*
* @return The current espnowEncryptionKey for this EspnowMeshBackend instance.
* @return The current espnowEncryptedConnectionKey for this EspnowMeshBackend instance.
*/
const uint8_t *getEspnowEncryptionKey();
uint8_t *getEspnowEncryptionKey(uint8_t resultArray[EspnowProtocolInterpreter::espnowEncryptionKeyLength]);
const uint8_t *getEspnowEncryptedConnectionKey();
uint8_t *getEspnowEncryptedConnectionKey(uint8_t resultArray[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength]);
/**
* Change the key used to encrypt/decrypt the encrypted connection key when creating encrypted ESP-NOW connections. (Kok = key of keys, perhaps) If no Kok is provided by the user, a default Kok is used.
* Will apply to any new encrypted connections.
* Must be called after begin() to take effect.
*
* NOTE: Encrypted connections added before the Kok change will retain their old Kok.
* This changes the Kok for all EspnowMeshBackend instances on this ESP8266.
* Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
* Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
*
* @param espnowEncryptionKok An array containing the espnowEncryptedConnectionKeyLength bytes that will be used as the Kok.
* @return True if Kok was changed successfully. False if Kok was not changed.
*/
static bool setEspnowEncryptionKok(uint8_t espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength]);
/**
* Change the key used to encrypt/decrypt the encryption key when creating encrypted ESP-NOW connections. (Kok = key of keys, perhaps) If no Kok is provided by the user, a default Kok is used.
@ -349,13 +415,14 @@ public:
*
* NOTE: Encrypted connections added before the Kok change will retain their old Kok.
* This changes the Kok for all EspnowMeshBackend instances on this ESP8266.
* Both Kok and encryption key must match in an encrypted connection pair for encrypted communication to be possible.
* Both Kok and encrypted connection key must match in an encrypted connection pair for encrypted communication to be possible.
* Otherwise the transmissions will never reach the recipient, even though acks are received by the sender.
*
* @param espnowEncryptionKok An array containing the espnowEncryptionKeyLength bytes that will be used as the Kok.
* @param espnowEncryptionKokSeed A string that will be used to generate the KoK. The same string will always generate the same KoK.
* A minimum of 8 random characters are recommended to ensure sufficient KoK variation.
* @return True if Kok was changed successfully. False if Kok was not changed.
*/
static bool setEspnowEncryptionKok(uint8_t espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptionKeyLength]);
static bool setEspnowEncryptionKok(const String &espnowEncryptionKokSeed);
/**
* Get the key used to encrypt the encryption keys when creating encrypted ESP-NOW connections. (Kok = key of keys, perhaps) Returns nullptr if no Kok has been provided by the user.
@ -364,7 +431,7 @@ public:
*/
static const uint8_t *getEspnowEncryptionKok();
/**
/**
* Change the secret key used to generate HMACs for encrypted ESP-NOW connections.
* Will apply to any new received requests for encrypted connection if this EspnowMeshBackend instance is the current request manager.
* Will apply to any new encrypted connections requested or added by this EspnowMeshBackend instance.
@ -375,12 +442,75 @@ public:
* @param espnowHashKey An array containing the espnowHashKeyLength bytes that will be used as the HMAC key.
*/
void setEspnowHashKey(const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength]);
/**
* Change the secret key used to generate HMACs for encrypted ESP-NOW connections.
* Will apply to any new received requests for encrypted connection if this EspnowMeshBackend instance is the current request manager.
* Will apply to any new encrypted connections requested or added by this EspnowMeshBackend instance.
*
* NOTE: Encrypted connections added before the key change will retain their old key.
* Only changes the secret hash key used by this EspnowMeshBackend instance, so each instance can use a separate secret key.
*
* @param espnowHashKeySeed A string that will be used to generate the HMAC key. The same string will always generate the same key.
* A minimum of 8 random characters are recommended to ensure sufficient key variation.
*/
void setEspnowHashKey(const String &espnowHashKeySeed);
const uint8_t *getEspnowHashKey();
/**
* Change the key used to encrypt/decrypt messages when using AEAD encryption.
* If no message encryption key is provided by the user, a default key consisting of all zeroes is used.
*
* This changes the message encryption key for all EspnowMeshBackend instances on this ESP8266.
*
* @param espnowMessageEncryptionKey An array containing the CryptoInterface::ENCRYPTION_KEY_LENGTH bytes that will be used as the message encryption key.
*/
static void setEspnowMessageEncryptionKey(uint8_t espnowMessageEncryptionKey[CryptoInterface::ENCRYPTION_KEY_LENGTH]);
/**
* Change the key used to encrypt/decrypt messages when using AEAD encryption.
* If no message encryption key is provided by the user, a default key consisting of all zeroes is used.
*
* This changes the message encryption key for all EspnowMeshBackend instances on this ESP8266.
*
* @param espnowMessageEncryptionKeySeed A string that will be used to generate the message encryption key. The same string will always generate the same key.
* A minimum of 8 random characters are recommended to ensure sufficient key variation.
*/
static void setEspnowMessageEncryptionKey(const String &espnowMessageEncryptionKeySeed);
/**
* Get the key used to encrypt/decrypt messages when using AEAD encryption.
*
* @return An uint8_t array with size CryptoInterface::ENCRYPTION_KEY_LENGTH containing the currently used message encryption key.
*/
static const uint8_t *getEspnowMessageEncryptionKey();
/**
* If true, AEAD will be used to encrypt/decrypt all messages sent/received by this node via ESP-NOW, regardless of whether the connection is encrypted or not.
* All nodes this node wishes to communicate with must then also use encrypted messages with the same getEspnowMessageEncryptionKey(), or messages will not be accepted.
* Note that using encrypted messages will reduce the number of message bytes that can be transmitted.
*
* Using AEAD will only encrypt the message content, not the transmission metadata.
* The AEAD encryption does not require any pairing, and is thus faster for single messages than establishing a new encrypted connection before transfer.
* AEAD encryption also works with ESP-NOW broadcasts and supports an unlimited number of nodes, which is not true for encrypted connections.
* Encrypted ESP-NOW connections do however come with built in replay attack protection, which is not provided by the framework when using AEAD encryption,
* and allow EspnowProtocolInterpreter::aeadMetadataSize extra message bytes per transmission.
* Transmissions via encrypted connections are also slightly faster than via AEAD once a connection has been established.
*
* useEncryptedMessages() is false by default.
*
* @param useEncryptedMessages If true, AEAD encryption/decryption is enabled. If false, AEAD encryption/decryption is disabled.
*/
static void setUseEncryptedMessages(bool useEncryptedMessages);
static bool useEncryptedMessages();
/**
* Hint: Use String.length() to get the ASCII length of a String.
*
* @return The maximum number of bytes (or ASCII characters) a transmission can contain. Note that non-ASCII characters usually require the space of at least two ASCII characters each.
* @return The maximum number of bytes (or ASCII characters) a transmission can contain.
* Note that non-ASCII characters usually require the space of at least two ASCII characters each.
* Also note that this value will be reduced by EspnowProtocolInterpreter::aeadMetadataSize if useEncryptedMessages() is true.
*/
static uint32_t getMaxMessageBytesPerTransmission();
@ -402,7 +532,9 @@ public:
/**
* Hint: Use String.length() to get the ASCII length of a String.
*
* @return The maximum length in bytes an ASCII message is allowed to be when transmitted/broadcasted by this node. Note that non-ASCII characters usually require at least two bytes each.
* @return The maximum length in bytes an ASCII message is allowed to be when transmitted/broadcasted by this node.
* Note that non-ASCII characters usually require at least two bytes each.
* Also note that this value will be reduced if useEncryptedMessages() is true.
*/
static uint32_t getMaxMessageLength();
@ -506,6 +638,19 @@ public:
void setBroadcastFilter(broadcastFilterType broadcastFilter);
broadcastFilterType getBroadcastFilter();
/**
* Set a function that should be called after each successful ESP-NOW response transmission, just before the response is removed from the waiting list.
* If a particular response is not sent, there will be no function call for it.
* Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called.
*
* The hook should return a bool.
* If this return value is true, the response transmission process will continue with the next response in the waiting list.
* If it is false, the response transmission process will stop after removing the just sent response from the waiting list.
* The default responseTransmittedHook always returns true.
*/
void setResponseTransmittedHook(responseTransmittedHookType responseTransmittedHook);
responseTransmittedHookType getResponseTransmittedHook();
/**
* Get the MAC address of the sender of the most recently received ESP-NOW request, response or broadcast to this EspnowMeshBackend instance.
@ -546,11 +691,11 @@ public:
uint8_t *getSenderAPMac(uint8_t *macArray);
/**
* Get whether the ESP-NOW request, response or broadcast which was most recently received by this EspnowMeshBackend instance was encrypted or not.
* Get whether the ESP-NOW request, response or broadcast which was most recently received by this EspnowMeshBackend instance was sent over an encrypted connection or not.
*
* @return If true, the request, response or broadcast was encrypted. If false, it was unencrypted.
* @return If true, the request, response or broadcast was sent over an encrypted connection. If false, the connection was unencrypted.
*/
bool receivedEncryptedMessage();
bool receivedEncryptedTransmission();
/**
* Should be used together with serializeUnencryptedConnection() if the node sends unencrypted transmissions
@ -563,10 +708,10 @@ public:
*/
static bool addUnencryptedConnection(const String &serializedConnectionState);
// Updates connection with current stored encryption key.
// Updates connection with current stored encrypted connection key.
// At least one of the leftmost 32 bits in each of the session keys should be 1, since the key otherwise indicates the connection is unencrypted.
encrypted_connection_status_t addEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey);
// Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized.
// Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey are not serialized.
// These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection.
// @param ignoreDuration Ignores any stored duration serializedConnectionState, guaranteeing that the created connection will be permanent. Returns: ECS_REQUEST_TRANSMISSION_FAILED indicates malformed serializedConnectionState.
encrypted_connection_status_t addEncryptedConnection(const String &serializedConnectionState, bool ignoreDuration = false);
@ -575,7 +720,7 @@ public:
// As with all these methods, changes will only take effect once the requester proves it has the ability to decrypt the session key.
// At least one of the leftmost 32 bits in each of the session keys should be 1, since the key otherwise indicates the connection is unencrypted.
encrypted_connection_status_t addTemporaryEncryptedConnection(uint8_t *peerStaMac, uint8_t *peerApMac, uint64_t peerSessionKey, uint64_t ownSessionKey, uint32_t duration);
// Note that the espnowEncryptionKey, espnowEncryptionKok and espnowHashKey are not serialized.
// Note that the espnowEncryptedConnectionKey, espnowEncryptionKok, espnowHashKey and espnowMessageEncryptionKey are not serialized.
// These will be set to the values of the EspnowMeshBackend instance that is adding the serialized encrypted connection.
// Uses duration argument instead of any stored duration in serializedConnectionState. Returns: ECS_REQUEST_TRANSMISSION_FAILED indicates malformed serializedConnectionState.
encrypted_connection_status_t addTemporaryEncryptedConnection(const String &serializedConnectionState, uint32_t duration);
@ -593,15 +738,17 @@ public:
encrypted_connection_removal_outcome_t requestEncryptedConnectionRemoval(uint8_t *peerMac);
/**
* Set whether this EspnowMeshBackend instance will accept unencrypted ESP-NOW requests or not, when acting as EspnowRequestManager.
* Set whether this EspnowMeshBackend instance will accept ESP-NOW requests from unencrypted connections or not, when acting as EspnowRequestManager.
* When set to false and combined with already existing encrypted connections, this can be used to ensure only encrypted transmissions are processed.
* When set to false it will also make it impossible to send unencrypted requests for encrypted connection to the node,
* When set to false it will also make it impossible to send requests for encrypted connection to the node over an unencrypted connection,
* which can be useful if too many such requests could otherwise be expected.
*
* @param acceptsUnencryptedRequests If and only if true, unencrypted requests will be processed when this EspnowMeshBackend instance is acting as EspnowRequestManager. True by default.
* True by default.
*
* @param acceptsUnverifiedRequests If and only if true, requests from unencrypted connections will be processed when this EspnowMeshBackend instance is acting as EspnowRequestManager.
*/
void setAcceptsUnencryptedRequests(bool acceptsUnencryptedRequests);
bool acceptsUnencryptedRequests();
void setAcceptsUnverifiedRequests(bool acceptsUnverifiedRequests);
bool acceptsUnverifiedRequests();
/**
* Set a soft upper limit on the number of encrypted connections this node can have when receiving encrypted connection requests.
@ -609,12 +756,14 @@ public:
* Each EspnowMeshBackend instance can have a separate value. The value used is that of the current EspnowRequestManager.
* The hard upper limit is 6 encrypted connections, mandated by the ESP-NOW API.
*
* Default is 6.
*
* When a request for encrypted connection is received from a node to which there is no existing permanent encrypted connection,
* and the number of encrypted connections exceeds the soft limit,
* this request will automatically be converted to an autoEncryptionRequest.
* This means it will be a temporary connection with very short duration (with default framework settings).
*
* @param softLimit The new soft limit. Valid values are 0 to 6. Default is 6.
* @param softLimit The new soft limit. Valid values are 0 to 6.
*/
void setEncryptedConnectionsSoftLimit(uint8_t softLimit);
uint8_t encryptedConnectionsSoftLimit();
@ -749,11 +898,11 @@ protected:
void setSenderAPMac(uint8_t *macArray);
/**
* Set whether the most recently received ESP-NOW request, response or broadcast is presented as having been encrypted or not.
* Set whether the most recently received ESP-NOW request, response or broadcast is presented as having been sent over an encrypted connection or not
*
* @param receivedEncryptedMessage If true, the request, response or broadcast is presented as having been encrypted.
* @param receivedEncryptedTransmission If true, the request, response or broadcast is presented as having been sent over an encrypted connection.
*/
void setReceivedEncryptedMessage(bool receivedEncryptedMessage);
void setReceivedEncryptedTransmission(bool receivedEncryptedTransmission);
static bool temporaryEncryptedConnectionToPermanent(uint8_t *peerMac);
@ -767,6 +916,11 @@ protected:
*/
static bool _espnowConnectionQueueMutex;
/**
* Will be true when no responsesToSend element should be removed.
*/
static bool _responsesToSendMutex;
/**
* Check if there is an ongoing ESP-NOW transmission in the library. Used to avoid interrupting transmissions.
*
@ -814,6 +968,9 @@ protected:
private:
EspnowMeshBackend(requestHandlerType requestHandler, responseHandlerType responseHandler, networkFilterType networkFilter, broadcastFilterType broadcastFilter,
const String &meshPassword, const String &ssidPrefix, const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel);
typedef std::function<String(const String &, const ExpiringTimeTracker &)> encryptionRequestBuilderType;
static String defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker);
static String flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker);
@ -908,6 +1065,7 @@ private:
static bool _espnowSendConfirmed;
broadcastFilterType _broadcastFilter;
responseTransmittedHookType _responseTransmittedHook = [](const String &, const uint8_t *, uint32_t, EspnowMeshBackend &){ return true; };
uint8_t _broadcastTransmissionRedundancy = 1;
@ -923,17 +1081,19 @@ private:
static bool usesConstantSessionKey(char messageType);
bool _acceptsUnencryptedRequests = true;
bool _acceptsUnverifiedRequests = true;
uint8_t _espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength] {0};
uint8_t _espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength] {0};
uint8_t _espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength] {0};
static uint8_t _espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptionKeyLength];
static uint8_t _espnowEncryptionKok[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength];
static bool _espnowEncryptionKokSet;
static uint32_t _unencryptedMessageID;
static uint8_t _espnowMessageEncryptionKey[CryptoInterface::ENCRYPTION_KEY_LENGTH];
static bool _useEncryptedMessages;
static uint32_t _unsynchronizedMessageID;
uint8_t _senderMac[6] = {0};
uint8_t _senderAPMac[6] = {0};
bool _receivedEncryptedMessage = false;
bool _receivedEncryptedTransmission = false;
static bool _espnowSendToNodeMutex;
static uint8_t _transmissionTargetBSSID[6];

View File

@ -25,30 +25,27 @@
#include "EspnowProtocolInterpreter.h"
#include "TypeConversionFunctions.h"
#include <algorithm>
#include "EspnowMeshBackend.h"
namespace EspnowProtocolInterpreter
{
const uint64_t uint64LeftmostBits = 0xFFFFFFFF00000000;
uint8_t espnowProtocolBytesSize()
{
uint8_t espnowMetadataSize()
{
return 16;
return espnowProtocolBytesSize + (EspnowMeshBackend::useEncryptedMessages() ? aeadMetadataSize : 0);
}
String espnowGetMessageContent(uint8_t *transmission, uint8_t transmissionLength)
String espnowGetMessageContent(uint8_t *transmissionDataArray, uint8_t transmissionLength)
{
if(transmissionLength < espnowProtocolBytesSize())
String messageContent = emptyString;
if(transmissionLength >= espnowMetadataSize())
{
return "";
}
else
{
// Ensure we have a NULL terminated character array so the String() constructor knows where to stop.
uint8_t bufferedTransmission[transmissionLength + 1];
std::copy_n(transmission, transmissionLength, bufferedTransmission);
bufferedTransmission[transmissionLength] = 0;
return String((char *)(bufferedTransmission + espnowProtocolBytesSize()));
uint8_t messageSize = transmissionLength - espnowMetadataSize();
messageContent = uint8ArrayToMultiString(transmissionDataArray + espnowMetadataSize(), messageSize);
}
return messageContent;
}
char espnowGetMessageType(const uint8_t *transmissionDataArray)
@ -79,22 +76,12 @@ namespace EspnowProtocolInterpreter
uint64_t espnowGetMessageID(const uint8_t *transmissionDataArray)
{
uint64_t outcome = 0;
for(int shiftingFortune = 56; shiftingFortune >= 0; shiftingFortune -= 8)
{
outcome |= ((uint64_t)transmissionDataArray[espnowMessageIDIndex + 7 - shiftingFortune/8] << shiftingFortune);
}
return outcome;
return uint8ArrayToUint64(transmissionDataArray + espnowMessageIDIndex);
}
uint8_t *espnowSetMessageID(uint8_t *transmissionDataArray, uint64_t messageID)
{
for(int shiftingFortune = 56; shiftingFortune >= 0; shiftingFortune -= 8)
{
transmissionDataArray[espnowMessageIDIndex + 7 - shiftingFortune/8] = messageID >> shiftingFortune & 0xFF;
}
return transmissionDataArray;
return uint64ToUint8Array(messageID, transmissionDataArray + espnowMessageIDIndex);
}
bool usesEncryption(uint64_t messageID)

View File

@ -54,12 +54,16 @@ namespace EspnowProtocolInterpreter
const uint8_t espnowTransmissionMacIndex = 2;
const uint8_t espnowMessageIDIndex = 8;
uint8_t espnowProtocolBytesSize();
constexpr uint8_t espnowProtocolBytesSize = 16;
constexpr uint8_t aeadMetadataSize = 28;
uint8_t espnowMetadataSize();
const uint8_t espnowEncryptionKeyLength = 16; // This is restricted to exactly 16 bytes by the ESP-NOW API. It should not be changed unless the ESP-NOW API is changed.
const uint8_t espnowEncryptedConnectionKeyLength = 16; // This is restricted to exactly 16 bytes by the ESP-NOW API. It should not be changed unless the ESP-NOW API is changed.
const uint8_t espnowHashKeyLength = 16; // This can be changed to any value up to 255. Common values are 16 and 32.
String espnowGetMessageContent(uint8_t *transmission, uint8_t transmissionLength);
constexpr uint64_t uint64LeftmostBits = 0xFFFFFFFF00000000;
String espnowGetMessageContent(uint8_t *transmissionDataArray, uint8_t transmissionLength);
char espnowGetMessageType(const uint8_t *transmissionDataArray);
uint8_t espnowGetTransmissionsRemaining(const uint8_t *transmissionDataArray);
bool espnowIsMessageStart(const uint8_t *transmissionDataArray);

View File

@ -41,7 +41,7 @@ void floodingMeshDelay(uint32_t durationMs)
}
}
FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: _espnowBackend(
@ -49,17 +49,35 @@ FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &mesh
[this](const String &response, MeshBackendBase &meshInstance){ return _defaultResponseHandler(response, meshInstance); },
[this](int numberOfNetworks, MeshBackendBase &meshInstance){ return _defaultNetworkFilter(numberOfNetworks, meshInstance); },
[this](String &firstTransmission, EspnowMeshBackend &meshInstance){ return _defaultBroadcastFilter(firstTransmission, meshInstance); },
meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
meshPassword, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
setMessageHandler(messageHandler);
restoreDefaultTransmissionOutcomesUpdateHook();
restoreDefaultResponseTransmittedHook();
}
FloodingMesh::FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed,
const String &ssidPrefix, const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: FloodingMesh(messageHandler, meshPassword, (const uint8_t[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength]){0},
(const uint8_t[EspnowProtocolInterpreter::espnowHashKeyLength]){0}, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
getEspnowMeshBackend().setEspnowEncryptedConnectionKey(espnowEncryptedConnectionKeySeed);
getEspnowMeshBackend().setEspnowHashKey(espnowHashKeySeed);
}
FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: FloodingMesh(messageHandler, meshPassword, espnowEncryptionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
: FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKey, espnowHashKey, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
loadMeshState(serializedMeshState);
}
FloodingMesh::FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed, const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode, uint8 meshWiFiChannel)
: FloodingMesh(messageHandler, meshPassword, espnowEncryptedConnectionKeySeed, espnowHashKeySeed, ssidPrefix, ssidSuffix, verboseMode, meshWiFiChannel)
{
loadMeshState(serializedMeshState);
}
@ -73,6 +91,9 @@ void FloodingMesh::begin()
{
// Initialise the mesh node
getEspnowMeshBackend().begin();
// Used for encrypted broadcasts
getEspnowMeshBackend().setEncryptedConnectionsSoftLimit(3);
availableFloodingMeshes.insert(this); // Returns std::pair<iterator,bool>
}
@ -118,7 +139,7 @@ String FloodingMesh::serializeMeshState()
{
using namespace JsonTranslator;
// Returns: {"meshState":{"connectionState":{"unencMsgID":"123"},"meshMsgCount":"123"}}
// Returns: {"meshState":{"connectionState":{"unsyncMsgID":"123"},"meshMsgCount":"123"}}
String connectionState = getEspnowMeshBackend().serializeUnencryptedConnection();
@ -138,21 +159,21 @@ void FloodingMesh::loadMeshState(const String &serializedMeshState)
String connectionState = "";
if(!getConnectionState(serializedMeshState, connectionState) || !getEspnowMeshBackend().addUnencryptedConnection(connectionState))
{
getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain unencryptedMessageID. Using default instead.");
getEspnowMeshBackend().warningPrint("WARNING! serializedMeshState did not contain unsynchronizedMessageID. Using default instead.");
}
}
String FloodingMesh::generateMessageID()
{
char messageCountArray[2] = { 0 };
sprintf(messageCountArray, "%04X", _messageCount++);
char messageCountArray[5] = { 0 };
snprintf(messageCountArray, 5, "%04X", _messageCount++);
uint8_t apMac[6] {0};
return macToString(WiFi.softAPmacAddress(apMac)) + String(messageCountArray); // We use the AP MAC address as ID since it is what shows up during WiFi scans
}
void FloodingMesh::broadcast(const String &message)
{
assert(message.length() <= maxUnencryptedMessageSize());
assert(message.length() <= maxUnencryptedMessageLength());
String messageID = generateMessageID();
@ -176,7 +197,7 @@ uint8_t FloodingMesh::getBroadcastReceptionRedundancy() { return _broadcastRecep
void FloodingMesh::encryptedBroadcast(const String &message)
{
assert(message.length() <= maxEncryptedMessageSize());
assert(message.length() <= maxEncryptedMessageLength());
String messageID = generateMessageID();
@ -185,7 +206,7 @@ void FloodingMesh::encryptedBroadcast(const String &message)
void FloodingMesh::encryptedBroadcastKernel(const String &message)
{
getEspnowMeshBackend().attemptAutoEncryptingTransmission(message);
getEspnowMeshBackend().attemptAutoEncryptingTransmission(message, true);
}
void FloodingMesh::clearMessageLogs()
@ -214,12 +235,12 @@ uint8_t *FloodingMesh::getOriginMac(uint8_t *macArray)
return macArray;
}
uint32_t FloodingMesh::maxUnencryptedMessageSize()
uint32_t FloodingMesh::maxUnencryptedMessageLength()
{
return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - (getEspnowMeshBackend().getMeshName().length() + 1); // Need room for mesh name + delimiter
}
uint32_t FloodingMesh::maxEncryptedMessageSize()
uint32_t FloodingMesh::maxEncryptedMessageLength()
{
// Need 1 extra delimiter character for maximum metadata efficiency (makes it possible to store exactly 18 MACs in metadata by adding an extra transmission)
return getEspnowMeshBackend().getMaxMessageLength() - MESSAGE_ID_LENGTH - 1;
@ -234,9 +255,11 @@ uint16_t FloodingMesh::messageLogSize() { return _messageLogSize; }
void FloodingMesh::setMetadataDelimiter(char metadataDelimiter)
{
// Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata
assert(metadataDelimiter < 48 || 57 < metadataDelimiter);
assert(metadataDelimiter < 65 || 70 < metadataDelimiter);
// Using HEX number characters as a delimiter is a bad idea regardless of broadcast type, since they are always in the broadcast metadata.
// We therefore check for those characters below.
assert(metadataDelimiter < '0' || '9' < metadataDelimiter);
assert(metadataDelimiter < 'A' || 'F' < metadataDelimiter);
assert(metadataDelimiter < 'a' || 'f' < metadataDelimiter);
_metadataDelimiter = metadataDelimiter;
}
@ -328,6 +351,12 @@ void FloodingMesh::restoreDefaultTransmissionOutcomesUpdateHook()
getEspnowMeshBackend().setTransmissionOutcomesUpdateHook([this](MeshBackendBase &meshInstance){ return _defaultTransmissionOutcomesUpdateHook(meshInstance); });
}
void FloodingMesh::restoreDefaultResponseTransmittedHook()
{
getEspnowMeshBackend().setResponseTransmittedHook([this](const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance)
{ return _defaultResponseTransmittedHook(response, recipientMac, responseIndex, meshInstance); });
}
/**
* Callback for when other nodes send you a request
*
@ -340,7 +369,7 @@ String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBa
(void)meshInstance; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
String broadcastTarget = "";
String remainingRequest = "";
String remainingRequest = request;
if(request.charAt(0) == metadataDelimiter())
{
@ -350,11 +379,7 @@ String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBa
return ""; // metadataDelimiter not found
broadcastTarget = request.substring(1, broadcastTargetEndIndex + 1); // Include delimiter
remainingRequest = request.substring(broadcastTargetEndIndex + 1);
}
else
{
remainingRequest = request;
remainingRequest.remove(0, broadcastTargetEndIndex + 1);
}
int32_t messageIDEndIndex = remainingRequest.indexOf(metadataDelimiter());
@ -367,15 +392,16 @@ String FloodingMesh::_defaultRequestHandler(const String &request, MeshBackendBa
if(insertCompletedMessageID(messageID))
{
uint8_t originMacArray[6] = { 0 };
setOriginMac(uint64ToMac(messageID >> 16, originMacArray));
setOriginMac(uint64ToMac(messageID >> 16, originMacArray)); // messageID consists of MAC + 16 bit counter
String message = remainingRequest.substring(messageIDEndIndex + 1);
String message = remainingRequest;
message.remove(0, messageIDEndIndex + 1); // This approach avoids the null value removal of substring()
if(getMessageHandler()(message, *this))
{
message = broadcastTarget + remainingRequest.substring(0, messageIDEndIndex + 1) + message;
assert(message.length() <= _espnowBackend.getMaxMessageLength());
_forwardingBacklog.emplace_back(message, getEspnowMeshBackend().receivedEncryptedMessage());
_forwardingBacklog.emplace_back(message, getEspnowMeshBackend().receivedEncryptedTransmission());
}
}
@ -501,3 +527,27 @@ bool FloodingMesh::_defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshI
return true;
}
/**
* Once passed to the setResponseTransmittedHook method of the ESP-NOW backend,
* this function will be called after each successful ESP-NOW response transmission, just before the response is removed from the waiting list.
* If a particular response is not sent, there will be no function call for it.
* Only the hook of the EspnowMeshBackend instance that is getEspnowRequestManager() will be called.
*
* @param response The sent response.
* @param recipientMac The MAC address the response was sent to.
* @param responseIndex The index of the response in the waiting list.
* @param meshInstance The EspnowMeshBackend instance that called the function.
*
* @return True if the response transmission process should continue with the next response in the waiting list.
* False if the response transmission process should stop after removing the just sent response from the waiting list.
*/
bool FloodingMesh::_defaultResponseTransmittedHook(const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance)
{
(void)response; // This is useful to remove a "unused parameter" compiler warning. Does nothing else.
(void)recipientMac;
(void)responseIndex;
(void)meshInstance;
return true;
}

View File

@ -55,7 +55,7 @@ public:
*
* @param messageHandler The callback handler responsible for dealing with messages received from the mesh.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptionKey An uint8_t array containing the key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowEncryptedConnectionKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKey An uint8_t array containing the secret key used by the EspnowMeshBackend instance to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
@ -68,7 +68,39 @@ public:
* make it impossible for other stations to detect the APs whose WiFi channels have changed.
*
*/
FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
/**
* FloodingMesh constructor method. Creates a FloodingMesh node, ready to be initialised.
*
* @param messageHandler The callback handler responsible for dealing with messages received from the mesh.
* @param meshPassword The WiFi password for the mesh network.
* @param espnowEncryptedConnectionKeySeed A string containing the seed that will generate the secret key used by the EspnowMeshBackend instance for creating encrypted ESP-NOW connections.
* @param espnowHashKeySeed A string containing the seed that will generate the secret key used by the EspnowMeshBackend to generate HMACs for encrypted ESP-NOW connections.
* @param ssidPrefix The prefix (first part) of the node SSID.
* @param ssidSuffix The suffix (last part) of the node SSID.
* @param verboseMode Determines if we should print the events occurring in the library to Serial. Off by default. This setting is shared by all EspnowMeshBackend instances.
* @param meshWiFiChannel The WiFi channel used by the mesh network. Valid values are integers from 1 to 13. Defaults to 1.
* WARNING: The ESP8266 has only one WiFi channel, and the the station/client mode is always prioritized for channel selection.
* This can cause problems if several mesh instances exist on the same ESP8266 and use different WiFi channels.
* In such a case, whenever the station of one mesh instance connects to an AP, it will silently force the
* WiFi channel of any active AP on the ESP8266 to match that of the station. This will cause disconnects and possibly
* make it impossible for other stations to detect the APs whose WiFi channels have changed.
*
*/
FloodingMesh(messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed, const String &espnowHashKeySeed,
const String &ssidPrefix, const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
/**
* This constructor should be used in combination with serializeMeshState() when the node has gone to sleep while other nodes stayed awake.
* Otherwise the message ID will be reset after sleep, which means that the nodes that stayed awake may ignore new broadcasts for a while.
*
* @param serializedMeshState A String with a serialized mesh node state that the node should use.
*/
FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const uint8_t espnowEncryptedConnectionKey[EspnowProtocolInterpreter::espnowEncryptedConnectionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
@ -78,18 +110,13 @@ public:
*
* @param serializedMeshState A String with a serialized mesh node state that the node should use.
*/
FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword,
const uint8_t espnowEncryptionKey[EspnowProtocolInterpreter::espnowEncryptionKeyLength],
const uint8_t espnowHashKey[EspnowProtocolInterpreter::espnowHashKeyLength], const String &ssidPrefix,
const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
FloodingMesh(const String &serializedMeshState, messageHandlerType messageHandler, const String &meshPassword, const String &espnowEncryptedConnectionKeySeed,
const String &espnowHashKeySeed, const String &ssidPrefix, const String &ssidSuffix, bool verboseMode = false, uint8 meshWiFiChannel = 1);
virtual ~FloodingMesh();
/**
* The method responsible for initialising this FloodingMesh instance.
*
* Since there is only one WiFi radio on the ESP8266, only the FloodingMesh instance that was the last to begin() will be visible to surrounding nodes.
* All FloodingMesh instances can still broadcast messages though, even if their AP is not visible.
*/
void begin();
@ -97,8 +124,9 @@ public:
* Makes it possible to find the node through scans, and also makes it possible to recover from an encrypted ESP-NOW connection where only the other node is encrypted.
* Required for encryptedBroadcast() usage, but also slows down the start-up of the node.
*
* Note that only one AP can be active at a time in total, and this will always be the one which was last activated.
* Note that only one AP can be active at a time in total (there is only one WiFi radio on the ESP8266), and this will always be the one which was last activated.
* Thus the AP is shared by all backends.
* All FloodingMesh instances can still broadcast messages though, even if their AP is not visible.
*/
void activateAP();
@ -123,10 +151,10 @@ public:
/**
* Make an unencrypted broadcast to the entire mesh network.
*
* It is recommended that there is at most one new message transmitted in the mesh every 10, 20, 30 ms for messages of length maxUnencryptedMessageSize()*n,
* It is recommended that there is at most one new message transmitted in the mesh every 10, 20, 30 ms for messages up to length maxUnencryptedMessageLength()*n,
* where n is (roughly, depending on mesh name length) 1/4, 3/5 and 1 respectively. If transmissions are more frequent than this, message loss will increase.
*
* @param message The message to broadcast. Maximum message length is given by maxUnencryptedMessageSize(). The longer the message, the longer the transmission time.
* @param message The message to broadcast. Maximum message length is given by maxUnencryptedMessageLength(). The longer the message, the longer the transmission time.
*/
void broadcast(const String &message);
@ -143,17 +171,24 @@ public:
* Make an encrypted broadcast to the entire mesh network.
*
* ########## WARNING! This an experimental feature. API may change at any time. Only use if you like it when things break. ##########
* Will be very slow compared to unencrypted broadcasts. Probably works OK in a small mesh with a maximum of one new message transmitted in the mesh every second.
* Will be very slow compared to unencrypted broadcasts. Probably works OK in a small mesh with a maximum of 2-3 new messages transmitted in the mesh every second.
* Because of the throughput difference, mixing encypted and unencrypted broadcasts is not recommended if there are frequent mesh broadcasts (multiple per second),
* since a lot of unencrypted broadcasts can build up while a single encrypted broadcast is sent.
*
* It is recommended that verboseMode is turned off if using this, to avoid slowdowns due to excessive Serial printing.
*
* @param message The message to broadcast. Maximum message length is given by maxEncryptedMessageSize(). The longer the message, the longer the transmission time.
* @param message The message to broadcast. Maximum message length is given by maxEncryptedMessageLength(). The longer the message, the longer the transmission time.
*/
void encryptedBroadcast(const String &message);
void clearMessageLogs();
/**
* Clear the logs used for remembering which messages this node has received from the mesh network.
*/
void clearMessageLogs();
/**
* Remove all messages received from the mesh network which are stored waiting to be forwarded by this node.
*/
void clearForwardingBacklog();
/**
@ -203,7 +238,7 @@ public:
* Note that non-ASCII characters usually require at least two bytes each.
* Also note that for unencrypted messages the maximum size will depend on getEspnowMeshBackend().getMeshName().length()
*/
uint32_t maxUnencryptedMessageSize();
uint32_t maxUnencryptedMessageLength();
/**
* Hint: Use String.length() to get the ASCII length of a String.
@ -211,14 +246,14 @@ public:
* @return The maximum length in bytes an encrypted ASCII message is allowed to be when broadcasted by this node.
* Note that non-ASCII characters usually require at least two bytes each.
*/
uint32_t maxEncryptedMessageSize();
uint32_t maxEncryptedMessageLength();
/**
* Set the delimiter character used for metadata by every FloodingMesh instance.
* Using characters found in the mesh name or in HEX numbers is unwise, as is using ','.
*
* @param metadataDelimiter The metadata delimiter character to use.
* Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII
* Defaults to 23 = End-of-Transmission-Block (ETB) control character in ASCII
*/
static void setMetadataDelimiter(char metadataDelimiter);
static char metadataDelimiter();
@ -236,6 +271,7 @@ public:
void restoreDefaultNetworkFilter();
void restoreDefaultBroadcastFilter();
void restoreDefaultTransmissionOutcomesUpdateHook();
void restoreDefaultResponseTransmittedHook();
protected:
@ -289,6 +325,7 @@ private:
void _defaultNetworkFilter(int numberOfNetworks, MeshBackendBase &meshInstance);
bool _defaultBroadcastFilter(String &firstTransmission, EspnowMeshBackend &meshInstance);
bool _defaultTransmissionOutcomesUpdateHook(MeshBackendBase &meshInstance);
bool _defaultResponseTransmittedHook(const String &response, const uint8_t *recipientMac, uint32_t responseIndex, EspnowMeshBackend &meshInstance);
};
#endif

View File

@ -248,9 +248,9 @@ namespace JsonTranslator
return true;
}
bool getUnencryptedMessageID(const String &jsonString, uint32_t &result)
bool getUnsynchronizedMessageID(const String &jsonString, uint32_t &result)
{
int32_t startIndex = getStartIndex(jsonString, jsonUnencryptedMessageID);
int32_t startIndex = getStartIndex(jsonString, jsonUnsynchronizedMessageID);
if(startIndex < 0)
return false;

View File

@ -39,7 +39,7 @@ namespace JsonTranslator
const String jsonNonce = "\"nonce\":";
const String jsonHmac = "\"hmac\":";
const String jsonDesync = "\"desync\":";
const String jsonUnencryptedMessageID = "\"unencMsgID\":";
const String jsonUnsynchronizedMessageID = "\"unsyncMsgID\":";
const String jsonMeshMessageCount = "\"meshMsgCount\":";
String createJsonPair(const String &valueIdentifier, const String &value);
@ -102,7 +102,7 @@ namespace JsonTranslator
bool getNonce(const String &jsonString, String &result);
bool getHmac(const String &jsonString, String &result);
bool getDesync(const String &jsonString, bool &result);
bool getUnencryptedMessageID(const String &jsonString, uint32_t &result);
bool getUnsynchronizedMessageID(const String &jsonString, uint32_t &result);
bool getMeshMessageCount(const String &jsonString, uint16_t &result);
}

View File

@ -193,7 +193,7 @@ public:
/**
* Set a function that should be called after each update of the latestTransmissionOutcomes vector during attemptTransmission. (which happens after each individual transmission has finished)
* The function should return a bool. If this return value is true, attemptTransmission will continue with the next entry in the connectionQueue. If it is false, attemptTransmission will stop.
* The hook should return a bool. If this return value is true, attemptTransmission will continue with the next entry in the connectionQueue. If it is false, attemptTransmission will stop.
* The default transmissionOutcomesUpdateHook always returns true.
*
* Example use cases is modifying getMessage() between transmissions, or aborting attemptTransmission before all nodes in the connectionQueue have been contacted.

View File

@ -23,6 +23,7 @@
*/
#include "MeshCryptoInterface.h"
#include <assert.h>
namespace MeshCryptoInterface
{
@ -39,4 +40,13 @@ namespace MeshCryptoInterface
else
return false;
}
uint8_t *initializeKey(uint8_t *key, uint8_t keyLength, const String &keySeed)
{
assert(keyLength <= CryptoInterface::SHA256_NATURAL_LENGTH);
uint8_t hashArray[CryptoInterface::SHA256_NATURAL_LENGTH] {};
CryptoInterface::sha256Hash(keySeed.c_str(), keySeed.length(), hashArray);
memcpy(key, hashArray, keyLength);
return key;
}
}

View File

@ -67,6 +67,17 @@ namespace MeshCryptoInterface
* @return True if the HMAC is correct. False otherwise.
*/
bool verifyMeshHmac(const String &message, const String &messageHmac, const uint8_t *hashKey, uint8_t hashKeyLength);
/**
* Initialize key with a SHA-256 hash of keySeed.
*
* @param key A uint8_t array containing the key to be initialized.
* @param keyLength The length of the key array in bytes. Maximum value is CryptoInterface::SHA256_NATURAL_LENGTH.
* @param keySeed The key seed.
*
* @return A pointer to the initialized key array.
*/
uint8_t *initializeKey(uint8_t *key, uint8_t keyLength, const String &keySeed);
}
#endif

View File

@ -1,6 +1,6 @@
/*
* TypeConversionFunctions
* Copyright (C) 2018 Anders Löfgren
* Copyright (C) 2018-2019 Anders Löfgren
*
* License (MIT license):
*
@ -25,19 +25,39 @@
#include "TypeConversionFunctions.h"
namespace
{
constexpr char chars[36] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
constexpr uint8_t charValues[75] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, // 0 to 10
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 0, 0, 0, 0, 0, 0, // Upper case letters
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35}; // Lower case letters
}
String uint64ToString(uint64_t number, byte base)
{
assert(2 <= base && base <= 36);
String result = "";
while(number > 0)
String result;
if(base == 16)
{
result = String((uint32_t)(number % base), base) + result;
number /= base;
do {
result += chars[ number % base ];
number >>= 4; // We could write number /= 16; and the compiler would optimize it to a shift, but the explicit shift notation makes it clearer where the speed-up comes from.
} while ( number );
}
else
{
do {
result += chars[ number % base ];
number /= base;
} while ( number );
}
return (result == "" ? "0" : result);
std::reverse( result.begin(), result.end() );
return result;
}
uint64_t stringToUint64(const String &string, byte base)
@ -46,12 +66,21 @@ uint64_t stringToUint64(const String &string, byte base)
uint64_t result = 0;
char currentCharacter[1];
for(uint32_t i = 0; i < string.length(); i++)
if(base == 16)
{
result *= base;
currentCharacter[0] = string.charAt(i);
result += strtoul(currentCharacter, NULL, base);
for(uint32_t i = 0; i < string.length(); ++i)
{
result <<= 4; // We could write result *= 16; and the compiler would optimize it to a shift, but the explicit shift notation makes it clearer where the speed-up comes from.
result += charValues[string.charAt(i) - '0'];
}
}
else
{
for(uint32_t i = 0; i < string.length(); ++i)
{
result *= base;
result += charValues[string.charAt(i) - '0'];
}
}
return result;
@ -59,33 +88,78 @@ uint64_t stringToUint64(const String &string, byte base)
String uint8ArrayToHexString(const uint8_t *uint8Array, uint32_t arrayLength)
{
char hexString[2*arrayLength + 1]; // Each uint8_t will become two characters (00 to FF) and we want a null terminated char array.
hexString[arrayLength + 1] = { 0 };
for(uint32_t i = 0; i < arrayLength; i++)
String hexString;
if(!hexString.reserve(2*arrayLength)) // Each uint8_t will become two characters (00 to FF)
return emptyString;
for(uint32_t i = 0; i < arrayLength; ++i)
{
sprintf(hexString + 2*i, "%02X", uint8Array[i]);
hexString += chars[ uint8Array[i] >> 4 ];
hexString += chars[ uint8Array[i] % 16 ];
}
return String(hexString);
return hexString;
}
uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, uint32_t arrayLength)
{
assert(hexString.length() >= arrayLength*2); // Each array element can hold two hexString characters
for(uint32_t i = 0; i < arrayLength; i++)
for(uint32_t i = 0; i < arrayLength; ++i)
{
uint8Array[i] = strtoul(hexString.substring(i*2, (i+1)*2).c_str(), nullptr, 16);
uint8Array[i] = (charValues[hexString.charAt(i*2) - '0'] << 4) + charValues[hexString.charAt(i*2 + 1) - '0'];
}
return uint8Array;
}
String uint8ArrayToMultiString(uint8_t *uint8Array, uint32_t arrayLength)
{
String multiString;
if(!multiString.reserve(arrayLength))
return emptyString;
// Ensure we have a NULL terminated character array so the String() constructor knows where to stop.
char finalChar = uint8Array[arrayLength - 1];
uint8Array[arrayLength - 1] = 0;
multiString += (char *)(uint8Array);
while(multiString.length() < arrayLength - 1)
{
multiString += (char)0; // String construction only stops for null values, so we need to add those manually.
multiString += (char *)(uint8Array + multiString.length());
}
multiString += finalChar;
uint8Array[arrayLength - 1] = finalChar;
return multiString;
}
String bufferedUint8ArrayToMultiString(const uint8_t *uint8Array, uint32_t arrayLength)
{
String multiString;
if(!multiString.reserve(arrayLength))
return emptyString;
// Ensure we have a NULL terminated character array so the String() constructor knows where to stop.
uint8_t bufferedData[arrayLength + 1];
std::copy_n(uint8Array, arrayLength, bufferedData);
bufferedData[arrayLength] = 0;
multiString += (char *)(bufferedData);
while(multiString.length() < arrayLength)
{
multiString += (char)0; // String construction only stops for null values, so we need to add those manually.
multiString += (char *)(bufferedData + multiString.length());
}
return multiString;
}
String macToString(const uint8_t *mac)
{
char macString[13] = { 0 };
sprintf(macString, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(macString);
return uint8ArrayToHexString(mac, 6);
}
uint8_t *stringToMac(const String &macString, uint8_t *macArray)
@ -95,25 +169,45 @@ uint8_t *stringToMac(const String &macString, uint8_t *macArray)
uint64_t macToUint64(const uint8_t *macArray)
{
uint64_t outcome = 0;
for(int shiftingFortune = 40; shiftingFortune >= 0; shiftingFortune -= 8)
{
outcome |= ((uint64_t)macArray[5 - shiftingFortune/8] << shiftingFortune);
}
return outcome;
uint64_t result = (uint64_t)macArray[0] << 40 | (uint64_t)macArray[1] << 32 | (uint64_t)macArray[2] << 24 | (uint64_t)macArray[3] << 16 | (uint64_t)macArray[4] << 8 | (uint64_t)macArray[5];
return result;
}
uint8_t *uint64ToMac(uint64_t macValue, uint8_t *macArray)
{
assert(macValue <= 0xFFFFFFFFFFFF); // Overflow will occur if value can't fit within 6 bytes
for(int shiftingFortune = 40; shiftingFortune >= 0; shiftingFortune -= 8)
{
macArray[5 - shiftingFortune/8] = macValue >> shiftingFortune & 0xFF;
}
macArray[5] = macValue;
macArray[4] = macValue >> 8;
macArray[3] = macValue >> 16;
macArray[2] = macValue >> 24;
macArray[1] = macValue >> 32;
macArray[0] = macValue >> 40;
return macArray;
}
}
uint8_t *uint64ToUint8Array(uint64_t value, uint8_t *resultArray)
{
resultArray[7] = value;
resultArray[6] = value >> 8;
resultArray[5] = value >> 16;
resultArray[4] = value >> 24;
resultArray[3] = value >> 32;
resultArray[2] = value >> 40;
resultArray[1] = value >> 48;
resultArray[0] = value >> 56;
return resultArray;
}
uint64_t uint8ArrayToUint64(const uint8_t *inputArray)
{
uint64_t result = (uint64_t)inputArray[0] << 56 | (uint64_t)inputArray[1] << 48 | (uint64_t)inputArray[2] << 40 | (uint64_t)inputArray[3] << 32
| (uint64_t)inputArray[4] << 24 | (uint64_t)inputArray[5] << 16 | (uint64_t)inputArray[6] << 8 | (uint64_t)inputArray[7];
return result;
}
/**
* Helper function for meshBackendCast.

View File

@ -1,6 +1,6 @@
/*
* TypeConversionFunctions
* Copyright (C) 2018 Anders Löfgren
* Copyright (C) 2018-2019 Anders Löfgren
*
* License (MIT license):
*
@ -33,7 +33,8 @@
#include "EspnowMeshBackend.h"
/**
* Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words.
* Note that using base 10 instead of 16 increases conversion time by roughly a factor of 5, due to unfavourable 64-bit arithmetic.
* Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words.
*
* @param number The number to convert to a string with radix "base".
* @param base The radix to convert "number" into. Must be between 2 and 36.
@ -42,6 +43,7 @@
String uint64ToString(uint64_t number, byte base = 16);
/**
* Note that using base 10 instead of 16 increases conversion time by roughly a factor of 2, due to unfavourable 64-bit arithmetic.
* Note that using a base higher than 16 increases likelihood of randomly generating SSID strings containing controversial words.
*
* @param string The string to convert to uint64_t. String must use radix "base".
@ -50,12 +52,53 @@ String uint64ToString(uint64_t number, byte base = 16);
*/
uint64_t stringToUint64(const String &string, byte base = 16);
// All array elements will be padded with zeroes to ensure they are converted to 2 string characters each.
/**
* Convert the contents of a uint8_t array to a String in HEX format. The resulting String starts from index 0 of the array.
* All array elements will be padded with zeroes to ensure they are converted to 2 String characters each.
*
* @param uint8Array The array to make into a HEX String.
* @param arrayLength The size of uint8Array, in bytes.
* @return Normally a String containing the HEX representation of the uint8Array. An empty String if the memory allocation for the String failed.
*/
String uint8ArrayToHexString(const uint8_t *uint8Array, uint32_t arrayLength);
// There must be 2 string characters for each array element. Use padding with zeroes where required.
/**
* Convert the contents of a String in HEX format to a uint8_t array. Index 0 of the array will represent the start of the String.
* There must be 2 String characters for each array element. Use padding with zeroes where required.
*
* @param hexString The HEX String to convert to a uint8_t array. Must contain at least 2*arrayLength characters.
* @param uint8Array The array to fill with the contents of the hexString.
* @param arrayLength The number of bytes to fill in uint8Array.
* @return A pointer to the uint8Array.
*/
uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, uint32_t arrayLength);
/**
* Stores the exact values of uint8Array in a String, even null values.
* Note that Strings containing null values will look like several separate Strings to methods that rely on null values to find the String end, such as String::substring.
* In these cases, it may be helpful to use String::c_str() or String::begin() to access the String data buffer directly instead.
*
* The unbuffered version temporarily edits uint8Array during execution, but restores the array to its original state when returning in a controlled manner.
*
* @param uint8Array The array to make into a multiString.
* @param arrayLength The size of uint8Array, in bytes.
* @return Normally a String containing the same data as the uint8Array. An empty String if the memory allocation for the String failed.
*/
String uint8ArrayToMultiString(uint8_t *uint8Array, uint32_t arrayLength);
/**
* Stores the exact values of uint8Array in a String, even null values.
* Note that Strings containing null values will look like several separate Strings to methods that rely on null values to find the String end, such as String::substring.
* In these cases, it may be helpful to use String::c_str() or String::begin() to access the String data buffer directly instead.
*
* The buffered version is slower and uses more memory than the unbuffered version, but can operate on const arrays.
*
* @param uint8Array The array to make into a multiString.
* @param arrayLength The size of uint8Array, in bytes.
* @return Normally a String containing the same data as the uint8Array. An empty String if the memory allocation for the String failed.
*/
String bufferedUint8ArrayToMultiString(const uint8_t *uint8Array, uint32_t arrayLength);
/**
* Takes a uint8_t array and converts the first 6 bytes to a hexadecimal string.
*
@ -90,6 +133,23 @@ uint64_t macToUint64(const uint8_t *macArray);
*/
uint8_t *uint64ToMac(uint64_t macValue, uint8_t *macArray);
/**
* Takes a uint64_t value and stores the bits in a uint8_t array. Assumes index 0 of the array should contain MSB.
*
* @param value The uint64_t value to convert to a uint8_t array.
* @param resultArray A uint8_t array that will hold the result once the function returns. Should have a size of at least 8 bytes.
* @return The resultArray.
*/
uint8_t *uint64ToUint8Array(uint64_t value, uint8_t *resultArray);
/**
* Takes a uint8_t array and converts the first 8 (lowest index) elements to a uint64_t. Assumes index 0 of the array contains MSB.
*
* @param inputArray A uint8_t array containing the data to convert to a uint64_t. Should have a size of at least 8 bytes.
* @return A uint64_t representation of the first 8 bytes of the array.
*/
uint64_t uint8ArrayToUint64(const uint8_t *inputArray);
/**
* Conversion function that can be used on MeshBackend classes instead of dynamic_cast since RTTI is disabled.
*